aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKrzysztof KosiƄski <krzysio@google.com>2022-10-25 18:14:30 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2022-10-25 18:14:30 +0000
commitcbf0e945fd31b17968521dcf591913b430904097 (patch)
tree24a77367abda0dc5905609d709d7dccfb5f66bf8
parent700b19c4f396e328230cc02f7e33345b380a2131 (diff)
parent9a7b7070bda4cfe673e15a5ca5c3fc26e1719e89 (diff)
downloadtruth-cbf0e945fd31b17968521dcf591913b430904097.tar.gz
Initial import of truth 1.1.3 am: 44918180b2 am: 9a7b7070bd
Original change: https://android-review.googlesource.com/c/platform/external/truth/+/2268426 Change-Id: Icebd1a86227a0055d93be7fd1ef0f911118d0722 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r--.github/dependabot.yml10
-rw-r--r--.github/workflows/ci.yml97
-rw-r--r--.gitignore10
-rw-r--r--CONTRIBUTING.md47
-rw-r--r--LICENSE202
-rw-r--r--METADATA17
-rw-r--r--MODULE_LICENSE_APACHE20
-rw-r--r--OWNERS3
-rw-r--r--README.md33
-rw-r--r--core/pom.xml186
-rw-r--r--core/src/main/java/com/google/common/truth/AbstractArraySubject.java65
-rw-r--r--core/src/main/java/com/google/common/truth/ActualValueInference.java1520
-rw-r--r--core/src/main/java/com/google/common/truth/AssertionErrorWithFacts.java63
-rw-r--r--core/src/main/java/com/google/common/truth/BigDecimalSubject.java100
-rw-r--r--core/src/main/java/com/google/common/truth/BooleanSubject.java52
-rw-r--r--core/src/main/java/com/google/common/truth/ClassSubject.java44
-rw-r--r--core/src/main/java/com/google/common/truth/ComparableSubject.java113
-rw-r--r--core/src/main/java/com/google/common/truth/ComparisonFailureWithFacts.java47
-rw-r--r--core/src/main/java/com/google/common/truth/ComparisonFailures.java139
-rw-r--r--core/src/main/java/com/google/common/truth/Correspondence.java841
-rw-r--r--core/src/main/java/com/google/common/truth/CustomSubjectBuilder.java73
-rw-r--r--core/src/main/java/com/google/common/truth/DiffUtils.java234
-rw-r--r--core/src/main/java/com/google/common/truth/DoubleSubject.java317
-rw-r--r--core/src/main/java/com/google/common/truth/ErrorWithFacts.java27
-rw-r--r--core/src/main/java/com/google/common/truth/Expect.java272
-rw-r--r--core/src/main/java/com/google/common/truth/ExpectFailure.java243
-rw-r--r--core/src/main/java/com/google/common/truth/Fact.java140
-rw-r--r--core/src/main/java/com/google/common/truth/FailureMetadata.java330
-rw-r--r--core/src/main/java/com/google/common/truth/FailureStrategy.java74
-rw-r--r--core/src/main/java/com/google/common/truth/FloatSubject.java323
-rw-r--r--core/src/main/java/com/google/common/truth/GraphMatching.java262
-rw-r--r--core/src/main/java/com/google/common/truth/GuavaOptionalSubject.java82
-rw-r--r--core/src/main/java/com/google/common/truth/IntegerSubject.java42
-rw-r--r--core/src/main/java/com/google/common/truth/IterableSubject.java2070
-rw-r--r--core/src/main/java/com/google/common/truth/LazyMessage.java65
-rw-r--r--core/src/main/java/com/google/common/truth/LongSubject.java82
-rw-r--r--core/src/main/java/com/google/common/truth/MapSubject.java788
-rw-r--r--core/src/main/java/com/google/common/truth/MathUtil.java64
-rw-r--r--core/src/main/java/com/google/common/truth/MultimapSubject.java828
-rw-r--r--core/src/main/java/com/google/common/truth/MultisetSubject.java43
-rw-r--r--core/src/main/java/com/google/common/truth/ObjectArraySubject.java38
-rw-r--r--core/src/main/java/com/google/common/truth/Ordered.java35
-rw-r--r--core/src/main/java/com/google/common/truth/Platform.java297
-rw-r--r--core/src/main/java/com/google/common/truth/PrimitiveBooleanArraySubject.java38
-rw-r--r--core/src/main/java/com/google/common/truth/PrimitiveByteArraySubject.java38
-rw-r--r--core/src/main/java/com/google/common/truth/PrimitiveCharArraySubject.java38
-rw-r--r--core/src/main/java/com/google/common/truth/PrimitiveDoubleArraySubject.java269
-rw-r--r--core/src/main/java/com/google/common/truth/PrimitiveFloatArraySubject.java274
-rw-r--r--core/src/main/java/com/google/common/truth/PrimitiveIntArraySubject.java38
-rw-r--r--core/src/main/java/com/google/common/truth/PrimitiveLongArraySubject.java38
-rw-r--r--core/src/main/java/com/google/common/truth/PrimitiveShortArraySubject.java38
-rw-r--r--core/src/main/java/com/google/common/truth/SimpleSubjectBuilder.java47
-rw-r--r--core/src/main/java/com/google/common/truth/StackTraceCleaner.java436
-rw-r--r--core/src/main/java/com/google/common/truth/StandardSubjectBuilder.java231
-rw-r--r--core/src/main/java/com/google/common/truth/StringSubject.java337
-rw-r--r--core/src/main/java/com/google/common/truth/Subject.java1198
-rw-r--r--core/src/main/java/com/google/common/truth/SubjectUtils.java405
-rw-r--r--core/src/main/java/com/google/common/truth/TableSubject.java112
-rw-r--r--core/src/main/java/com/google/common/truth/ThrowableSubject.java85
-rw-r--r--core/src/main/java/com/google/common/truth/Truth.gwt.xml31
-rw-r--r--core/src/main/java/com/google/common/truth/Truth.java313
-rw-r--r--core/src/main/java/com/google/common/truth/TruthFailureSubject.java187
-rw-r--r--core/src/main/java/com/google/common/truth/TruthJUnit.java76
-rw-r--r--core/src/main/java/com/google/common/truth/package-info.java34
-rw-r--r--core/src/main/java/com/google/common/truth/super/com/google/common/truth/Platform.java236
-rw-r--r--core/src/test/java/com/google/common/truth/ActualValueInferenceTest.java242
-rw-r--r--core/src/test/java/com/google/common/truth/BaseSubjectTestCase.java37
-rw-r--r--core/src/test/java/com/google/common/truth/BigDecimalSubjectTest.java89
-rw-r--r--core/src/test/java/com/google/common/truth/BooleanSubjectTest.java73
-rw-r--r--core/src/test/java/com/google/common/truth/ChainingTest.java290
-rw-r--r--core/src/test/java/com/google/common/truth/ClassSubjectTest.java57
-rw-r--r--core/src/test/java/com/google/common/truth/ComparableSubjectCompileTest.java97
-rw-r--r--core/src/test/java/com/google/common/truth/ComparableSubjectTest.java247
-rw-r--r--core/src/test/java/com/google/common/truth/ComparisonFailureWithFactsTest.java336
-rw-r--r--core/src/test/java/com/google/common/truth/CorrespondenceExceptionStoreTest.java117
-rw-r--r--core/src/test/java/com/google/common/truth/CorrespondenceTest.java666
-rw-r--r--core/src/test/java/com/google/common/truth/CustomFailureMessageTest.java110
-rw-r--r--core/src/test/java/com/google/common/truth/DoubleSubjectTest.java596
-rw-r--r--core/src/test/java/com/google/common/truth/ExpectFailureNonRuleTest.java137
-rw-r--r--core/src/test/java/com/google/common/truth/ExpectFailureRuleTest.java67
-rw-r--r--core/src/test/java/com/google/common/truth/ExpectFailureTest.java170
-rw-r--r--core/src/test/java/com/google/common/truth/ExpectFailureWithStackTraceTest.java67
-rw-r--r--core/src/test/java/com/google/common/truth/ExpectTest.java255
-rw-r--r--core/src/test/java/com/google/common/truth/ExpectWithStackTest.java160
-rw-r--r--core/src/test/java/com/google/common/truth/FactTest.java92
-rw-r--r--core/src/test/java/com/google/common/truth/FloatSubjectTest.java608
-rw-r--r--core/src/test/java/com/google/common/truth/GraphMatchingTest.java459
-rw-r--r--core/src/test/java/com/google/common/truth/GuavaOptionalSubjectTest.java100
-rw-r--r--core/src/test/java/com/google/common/truth/IntegerSubjectTest.java278
-rw-r--r--core/src/test/java/com/google/common/truth/IterableSubjectCorrespondenceTest.java2045
-rw-r--r--core/src/test/java/com/google/common/truth/IterableSubjectTest.java1212
-rw-r--r--core/src/test/java/com/google/common/truth/LongSubjectTest.java135
-rw-r--r--core/src/test/java/com/google/common/truth/MapSubjectTest.java2167
-rw-r--r--core/src/test/java/com/google/common/truth/MathUtilTest.java112
-rw-r--r--core/src/test/java/com/google/common/truth/MultimapSubjectTest.java1879
-rw-r--r--core/src/test/java/com/google/common/truth/MultisetSubjectTest.java55
-rw-r--r--core/src/test/java/com/google/common/truth/NoJUnitTest.java39
-rw-r--r--core/src/test/java/com/google/common/truth/ObjectArraySubjectTest.java292
-rw-r--r--core/src/test/java/com/google/common/truth/PlatformBaseSubjectTestCase.java23
-rw-r--r--core/src/test/java/com/google/common/truth/PrimitiveBooleanArraySubjectTest.java94
-rw-r--r--core/src/test/java/com/google/common/truth/PrimitiveByteArraySubjectTest.java105
-rw-r--r--core/src/test/java/com/google/common/truth/PrimitiveCharArraySubjectTest.java100
-rw-r--r--core/src/test/java/com/google/common/truth/PrimitiveDoubleArraySubjectTest.java667
-rw-r--r--core/src/test/java/com/google/common/truth/PrimitiveFloatArraySubjectTest.java726
-rw-r--r--core/src/test/java/com/google/common/truth/PrimitiveIntArraySubjectTest.java142
-rw-r--r--core/src/test/java/com/google/common/truth/PrimitiveLongArraySubjectTest.java100
-rw-r--r--core/src/test/java/com/google/common/truth/PrimitiveShortArraySubjectTest.java113
-rw-r--r--core/src/test/java/com/google/common/truth/StackTraceCleanerTest.java467
-rw-r--r--core/src/test/java/com/google/common/truth/StandardSubjectBuilderTest.java32
-rw-r--r--core/src/test/java/com/google/common/truth/StringSubjectTest.java550
-rw-r--r--core/src/test/java/com/google/common/truth/SubjectTest.java814
-rw-r--r--core/src/test/java/com/google/common/truth/TableSubjectTest.java164
-rw-r--r--core/src/test/java/com/google/common/truth/TestCorrespondences.java360
-rw-r--r--core/src/test/java/com/google/common/truth/TestPlatform.java23
-rw-r--r--core/src/test/java/com/google/common/truth/ThrowableSubjectTest.java140
-rw-r--r--core/src/test/java/com/google/common/truth/TruthAssertThatTest.java79
-rw-r--r--core/src/test/java/com/google/common/truth/TruthFailureSubjectTest.java185
-rw-r--r--core/src/test/java/com/google/common/truth/extension/Employee.java44
-rw-r--r--core/src/test/java/com/google/common/truth/extension/EmployeeSubject.java86
-rw-r--r--core/src/test/java/com/google/common/truth/extension/EmployeeSubjectTest.java64
-rw-r--r--core/src/test/java/com/google/common/truth/extension/FakeHrDatabase.java58
-rw-r--r--core/src/test/java/com/google/common/truth/extension/FakeHrDatabaseTest.java77
-rw-r--r--core/src/test/java/com/google/common/truth/extension/HrDatabase.java35
-rw-r--r--core/src/test/java/com/google/common/truth/gwt/Inventory.java84
-rw-r--r--core/src/test/java/com/google/common/truth/gwt/TruthGwtTest.java149
-rw-r--r--core/src/test/java/com/google/common/truth/gwt/TruthTest.gwt.xml8
-rwxr-xr-xcore/src/test/java/com/google/common/truth/no_junit_test.sh6
-rw-r--r--core/src/test/java/com/google/common/truth/super/com/google/common/truth/PlatformBaseSubjectTestCase.java35
-rw-r--r--core/src/test/java/com/google/common/truth/super/com/google/common/truth/TestPlatform.java23
-rw-r--r--extensions/java8/pom.xml61
-rw-r--r--extensions/java8/src/main/java/com/google/common/truth/IntStreamSubject.java233
-rw-r--r--extensions/java8/src/main/java/com/google/common/truth/LongStreamSubject.java233
-rw-r--r--extensions/java8/src/main/java/com/google/common/truth/OptionalDoubleSubject.java84
-rw-r--r--extensions/java8/src/main/java/com/google/common/truth/OptionalIntSubject.java77
-rw-r--r--extensions/java8/src/main/java/com/google/common/truth/OptionalLongSubject.java77
-rw-r--r--extensions/java8/src/main/java/com/google/common/truth/OptionalSubject.java87
-rw-r--r--extensions/java8/src/main/java/com/google/common/truth/PathSubject.java33
-rw-r--r--extensions/java8/src/main/java/com/google/common/truth/StreamSubject.java229
-rw-r--r--extensions/java8/src/main/java/com/google/common/truth/Truth8.gwt.xml28
-rw-r--r--extensions/java8/src/main/java/com/google/common/truth/Truth8.java86
-rw-r--r--extensions/java8/src/test/java/com/google/common/truth/ExpectFailure8Test.java52
-rw-r--r--extensions/java8/src/test/java/com/google/common/truth/FailureAssertions.java35
-rw-r--r--extensions/java8/src/test/java/com/google/common/truth/IntStreamSubjectTest.java353
-rw-r--r--extensions/java8/src/test/java/com/google/common/truth/LongStreamSubjectTest.java394
-rw-r--r--extensions/java8/src/test/java/com/google/common/truth/OptionalDoubleSubjectTest.java101
-rw-r--r--extensions/java8/src/test/java/com/google/common/truth/OptionalIntSubjectTest.java101
-rw-r--r--extensions/java8/src/test/java/com/google/common/truth/OptionalLongSubjectTest.java101
-rw-r--r--extensions/java8/src/test/java/com/google/common/truth/OptionalSubjectTest.java114
-rw-r--r--extensions/java8/src/test/java/com/google/common/truth/PathSubjectTest.java31
-rw-r--r--extensions/java8/src/test/java/com/google/common/truth/StreamSubjectTest.java365
-rw-r--r--extensions/liteproto/pom.xml95
-rw-r--r--extensions/liteproto/src/main/java/com/google/common/truth/extensions/proto/LiteProtoSubject.java242
-rw-r--r--extensions/liteproto/src/main/java/com/google/common/truth/extensions/proto/LiteProtoTruth.java49
-rw-r--r--extensions/liteproto/src/test/java/com/google/common/truth/extensions/proto/LiteProtoSubjectTest.java299
-rw-r--r--extensions/liteproto/src/test/proto/test_message_lite2.proto26
-rw-r--r--extensions/liteproto/src/test/proto/test_message_lite3.proto21
-rw-r--r--extensions/pom.xml26
-rw-r--r--extensions/proto/pom.xml97
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/AnyUtils.java97
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/DiffResult.java664
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldDescriptorValidator.java87
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldNumberTree.java136
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScope.java192
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeImpl.java227
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeLogic.java567
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeLogicContainer.java34
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeLogicMap.java111
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeResult.java88
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeUtil.java131
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopes.java205
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FluentEqualityConfig.java486
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/IterableOfProtosFluentAssertion.java503
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/IterableOfProtosSubject.java1110
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/IterableOfProtosUsingCorrespondence.java185
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/MapWithProtoValuesFluentAssertion.java551
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/MapWithProtoValuesSubject.java905
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/MultimapWithProtoValuesFluentAssertion.java556
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/MultimapWithProtoValuesSubject.java943
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoFluentAssertion.java512
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoSubject.java907
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoSubjectBuilder.java72
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoTruth.java99
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoTruthMessageDifferencer.java971
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/RecursableDiffEntity.java247
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/SubScopeId.java55
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/UnknownFieldDescriptor.java100
-rw-r--r--extensions/proto/src/main/java/com/google/common/truth/extensions/proto/package-info.java27
-rw-r--r--extensions/proto/src/test/java/com/google/common/truth/extensions/proto/FieldScopesTest.java1109
-rw-r--r--extensions/proto/src/test/java/com/google/common/truth/extensions/proto/IterableOfProtosSubjectTest.java532
-rw-r--r--extensions/proto/src/test/java/com/google/common/truth/extensions/proto/MapWithProtoValuesSubjectTest.java256
-rw-r--r--extensions/proto/src/test/java/com/google/common/truth/extensions/proto/MultiExpectFailure.java68
-rw-r--r--extensions/proto/src/test/java/com/google/common/truth/extensions/proto/MultimapWithProtoValuesSubjectTest.java329
-rw-r--r--extensions/proto/src/test/java/com/google/common/truth/extensions/proto/OverloadResolutionTest.java363
-rw-r--r--extensions/proto/src/test/java/com/google/common/truth/extensions/proto/ProtoSubjectTest.java881
-rw-r--r--extensions/proto/src/test/java/com/google/common/truth/extensions/proto/ProtoSubjectTestBase.java289
-rw-r--r--extensions/proto/src/test/proto/test_message2.proto69
-rw-r--r--extensions/proto/src/test/proto/test_message3.proto69
-rw-r--r--extensions/re2j/pom.xml36
-rw-r--r--extensions/re2j/src/main/java/com/google/common/truth/extensions/re2j/Re2jSubjects.java141
-rw-r--r--extensions/re2j/src/test/java/com/google/common/truth/extensions/re2j/Re2jSubjectsTest.java71
-rw-r--r--overview.html14
-rw-r--r--pom.xml412
-rw-r--r--refactorings/src/main/java/com/google/common/truth/refactorings/CorrespondenceSubclassToFactoryCall.java658
-rw-r--r--refactorings/src/main/java/com/google/common/truth/refactorings/FailWithFacts.java152
-rw-r--r--refactorings/src/main/java/com/google/common/truth/refactorings/NamedToWithMessage.java268
-rw-r--r--refactorings/src/main/java/com/google/common/truth/refactorings/StoreActualValueInField.java281
-rw-r--r--refactorings/src/test/java/com/google/common/truth/refactorings/CorrespondenceSubclassToFactoryCallTest.java77
-rw-r--r--refactorings/src/test/java/com/google/common/truth/refactorings/FailWithFactsTest.java39
-rw-r--r--refactorings/src/test/java/com/google/common/truth/refactorings/NamedToWithMessageTest.java44
-rw-r--r--refactorings/src/test/java/com/google/common/truth/refactorings/StoreActualValueInFieldTest.java57
-rwxr-xr-xutil/generate-latest-docs.sh49
-rwxr-xr-xutil/mvn-deploy.sh53
212 files changed, 54471 insertions, 0 deletions
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..b76b8957
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,10 @@
+version: 2
+updates:
+ - package-ecosystem: "maven"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "daily"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..0b3b3e8b
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,97 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ branches:
+ - master
+
+jobs:
+ test:
+ name: "JDK ${{ matrix.java }}"
+ strategy:
+ matrix:
+ java: [ 8, 11 ]
+ runs-on: ubuntu-latest
+ steps:
+ # Cancel any previous runs for the same branch that are still running.
+ - name: 'Cancel previous runs'
+ uses: styfle/cancel-workflow-action@0.9.0
+ with:
+ access_token: ${{ github.token }}
+ - name: 'Check out repository'
+ uses: actions/checkout@v2
+ - name: 'Cache local Maven repository'
+ uses: actions/cache@v2.1.5
+ with:
+ path: ~/.m2/repository
+ key: maven-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ maven-
+ - name: 'Set up JDK ${{ matrix.java }}'
+ uses: actions/setup-java@v2
+ with:
+ java-version: ${{ matrix.java }}
+ distribution: 'zulu'
+ - name: 'Install'
+ shell: bash
+ run: mvn -B -P!standard-with-extra-repos install -U -DskipTests=true
+ - name: 'Test'
+ shell: bash
+ run: mvn -B -P!standard-with-extra-repos verify -U -Dmaven.javadoc.skip=true
+
+ publish_snapshot:
+ name: 'Publish snapshot'
+ needs: test
+ if: github.event_name == 'push' && github.repository == 'google/truth'
+ runs-on: ubuntu-latest
+ steps:
+ - name: 'Check out repository'
+ uses: actions/checkout@v2
+ - name: 'Cache local Maven repository'
+ uses: actions/cache@v2.1.5
+ with:
+ path: ~/.m2/repository
+ key: maven-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ maven-
+ - name: 'Set up JDK 11'
+ uses: actions/setup-java@v2
+ with:
+ java-version: 11
+ distribution: 'zulu'
+ server-id: sonatype-nexus-snapshots
+ server-username: CI_DEPLOY_USERNAME
+ server-password: CI_DEPLOY_PASSWORD
+ - name: 'Publish'
+ env:
+ CI_DEPLOY_USERNAME: ${{ secrets.CI_DEPLOY_USERNAME }}
+ CI_DEPLOY_PASSWORD: ${{ secrets.CI_DEPLOY_PASSWORD }}
+ run: mvn -B clean source:jar javadoc:jar deploy -DskipTests=true
+
+ generate_docs:
+ name: 'Generate latest docs'
+ needs: test
+ if: github.event_name == 'push' && github.repository == 'google/truth'
+ runs-on: ubuntu-latest
+ steps:
+ - name: 'Check out repository'
+ uses: actions/checkout@v2
+ - name: 'Cache local Maven repository'
+ uses: actions/cache@v2.1.5
+ with:
+ path: ~/.m2/repository
+ key: maven-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ maven-
+ - name: 'Set up JDK 11'
+ uses: actions/setup-java@v2
+ with:
+ java-version: 11
+ distribution: 'zulu'
+ - name: 'Generate latest docs'
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: ./util/generate-latest-docs.sh
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..f0486bef
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+bin
+target
+.project
+.classpath
+.settings
+.clover
+.*
+*.iml
+*.ipr
+*.iws
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..51bafc62
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,47 @@
+# How to contribute
+
+We'd love to accept your patches and contributions to this project. There are a
+just a few small guidelines you need to follow.
+
+## Contributor License Agreement
+
+Contributions to any Google project must be accompanied by a Contributor License
+Agreement. This is not a copyright **assignment**, it simply gives Google
+permission to use and redistribute your contributions as part of the project.
+
+When submitting a pull request, if you have not already signed the
+[Contributor License Agreement (CLA)][CLA], then a bot will remind you.
+Code cannot even be evaluated without this step.
+
+## Submitting a patch
+
+1. It's generally best to start by opening a new issue describing the bug or
+ feature you're intending to fix. Even if you think it's relatively minor,
+ it's helpful to know what people are working on. Mention in the initial
+ issue that you are planning to work on that bug or feature so that it can be
+ assigned to you.
+
+2. Follow the normal process of [forking] the project, and setup a new branch
+ to work in. It's important that each group of changes be done in separate
+ branches in order to ensure that a pull request only includes the commits
+ related to that bug or feature.
+
+3. Any significant changes should almost always be accompanied by tests. The
+ project already has good test coverage, so look at some of the existing
+ tests if you're unsure how to go about it.
+
+4. All contributions must be licensed Apache 2.0 and all files must have a copy
+ of the boilerplate licence comment (can be copied from an existing file).
+ Files should be formatted according to Google's [java style guide].
+
+5. Do your best to have [well-formed commit messages] for each change. This
+ provides consistency throughout the project, and ensures that commit
+ messages are able to be formatted properly by various git tools.
+
+6. Finally, push the commits to your fork and submit a [pull request].
+
+[CLA]: https://cla.developers.google.com
+[forking]: https://help.github.com/articles/fork-a-repo
+[java style guide]: https://google.github.io/styleguide/javaguide.html
+[well-formed commit messages]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
+[pull request]: https://help.github.com/articles/creating-a-pull-request
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..d6456956
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed 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.
diff --git a/METADATA b/METADATA
new file mode 100644
index 00000000..483212f2
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,17 @@
+name: "truth"
+description:
+ "Fluent assertions for Java and Android"
+
+third_party {
+ url {
+ type: HOMEPAGE
+ value: "https://truth.dev/"
+ }
+ url {
+ type: GIT
+ value: "https://github.com/google/truth.git"
+ }
+ version: "release_1_1_3"
+ last_upgrade_date { year: 2022 month: 10 day: 13 }
+ license_type: NOTICE
+}
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 00000000..7016fbac
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,3 @@
+krzysio@google.com
+ccross@android.com
+dwillemsen@google.com
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..dc3bf487
--- /dev/null
+++ b/README.md
@@ -0,0 +1,33 @@
+[![Main Site][gh-pages-shield]][gh-pages-link]
+[![Build Status][ci-shield]][ci-link]
+[![Maven Release][maven-shield]][maven-link]
+[![Stackoverflow][stackoverflow-shield]][stackoverflow-link]
+
+## What is Truth?
+
+Truth makes your [test assertions] and [failure messages] more readable.
+[Similar][comparison] to [AssertJ], it [natively supports][known_types] many JDK
+and [Guava] types, and it is [extensible][extension] to others.
+
+Truth is owned and maintained by the [Guava] team. It is used in the majority
+of the tests in Google’s own codebase.
+
+Read more at [the main website](https://truth.dev).
+
+<!-- references -->
+
+[test assertions]: https://truth.dev/benefits#readable-assertions
+[failure messages]: https://truth.dev/benefits#readable-messages
+[comparison]: https://truth.dev/comparison
+[AssertJ]: http://joel-costigliola.github.io/assertj/
+[known_types]: https://truth.dev/known_types
+[extension]: https://truth.dev/extension
+[Guava]: https://github.com/google/guava
+[gh-pages-shield]: https://img.shields.io/badge/main%20site-truth.dev-ff55ff.png?style=flat
+[gh-pages-link]: https://truth.dev/
+[ci-shield]: https://github.com/google/truth/workflows/CI/badge.svg?branch=master
+[ci-link]: https://github.com/google/truth/actions
+[maven-shield]: https://img.shields.io/maven-central/v/com.google.truth/truth.png
+[maven-link]: https://search.maven.org/artifact/com.google.truth/truth
+[stackoverflow-shield]: https://img.shields.io/badge/stackoverflow-google‐truth-5555ff.png?style=flat
+[stackoverflow-link]: https://stackoverflow.com/questions/tagged/google-truth
diff --git a/core/pom.xml b/core/pom.xml
new file mode 100644
index 00000000..f600f1c7
--- /dev/null
+++ b/core/pom.xml
@@ -0,0 +1,186 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>com.google.truth</groupId>
+ <artifactId>truth-parent</artifactId>
+ <version>HEAD-SNAPSHOT</version>
+ </parent>
+ <artifactId>truth</artifactId>
+ <name>Truth Core</name>
+ <dependencies>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.checkerframework</groupId>
+ <artifactId>checker-qual</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ </dependency>
+ <!-- Required only to test the -gwt sub-artifact. -->
+ <dependency>
+ <groupId>com.google.gwt</groupId>
+ <artifactId>gwt-user</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.google.auto.value</groupId>
+ <artifactId>auto-value-annotations</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava-gwt</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava-testlib</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.google.testing.compile</groupId>
+ <artifactId>compile-testing</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.google.errorprone</groupId>
+ <artifactId>error_prone_annotations</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.ow2.asm</groupId>
+ <artifactId>asm</artifactId>
+ <!-- Truth works fine without ASM: It just produces slightly less useful failure messages. Thus, users can exclude ASM if they prefer to reduce dependencies. (For example, ASM will not benefit anyone whose tests run in an Android VM.) Still, we have made it a dependency by default (non-<optional>), both because it provides useful functionality and because we have heard of problems with R8 when it is absent. -->
+ </dependency>
+ </dependencies>
+ <build>
+ <resources>
+ <resource>
+ <directory>src/main/java</directory>
+ <excludes>
+ <exclude>**/*.java</exclude>
+ <exclude>**/*.gwt.xml</exclude>
+ </excludes>
+ </resource>
+ </resources>
+ <testResources>
+ <testResource><directory>src/test/java</directory></testResource>
+ </testResources>
+ <plugins>
+ <plugin>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <excludes>
+ <exclude>**/super/**/*.java</exclude>
+ </excludes>
+ <testExcludes>
+ <testExclude>**/super/**/*.java</testExclude>
+ </testExcludes>
+ <annotationProcessorPaths>
+ <path>
+ <groupId>com.google.auto.value</groupId>
+ <artifactId>auto-value</artifactId>
+ <version>${auto-value.version}</version>
+ </path>
+ </annotationProcessorPaths>
+ </configuration>
+ </plugin>
+ <plugin>
+ <artifactId>maven-source-plugin</artifactId>
+ <configuration>
+ <excludes>
+ <exclude>**/super/**</exclude>
+ <exclude>**/*.gwt.xml</exclude>
+ </excludes>
+ </configuration>
+ </plugin>
+ <plugin>
+ <artifactId>maven-jar-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>attach-gwt-sources</id>
+ <phase>post-integration-test</phase>
+ <goals><goal>jar</goal></goals>
+ <configuration>
+ <classifier>gwt</classifier>
+ <classesDirectory>src/main/java</classesDirectory>
+ <includes>
+ <include>**/*.java</include>
+ <include>**/*.gwt.xml</include>
+ </includes>
+ <excludes>
+ <exclude>com/google/common/truth/ClassSubject.java</exclude>
+ <exclude>com/google/common/truth/Expect.java</exclude>
+ <exclude>com/google/common/truth/IteratingVerb.java</exclude>
+ <exclude>com/google/common/truth/ReflectionUtil.java</exclude>
+ <exclude>com/google/common/truth/codegen/**</exclude>
+ </excludes>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>animal-sniffer-maven-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <excludes>
+ <exclude>**/*GwtTest.java</exclude>
+ </excludes>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>gwt-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>gwt-test</id>
+ <goals><goal>test</goal></goals>
+ <configuration>
+ <mode>htmlunit</mode>
+ <htmlunit>FF38</htmlunit>
+ <productionMode>true</productionMode>
+ <!-- Fix OutOfMemoryError in Travis. -->
+ <extraJvmArgs>-Xms3500m -Xmx3500m -Xss1024k</extraJvmArgs>
+ <sourceLevel>auto</sourceLevel>
+ <userAgents>gecko1_8</userAgents>
+ <includes>**/*GwtTest.java</includes>
+ <!-- Keep these timeouts very large because, if we hit the timeout, the tests silently pass :( -->
+ <testTimeOut>86400 <!-- seconds --></testTimeOut>
+ <testMethodTimeout>1440 <!-- minutes --></testMethodTimeout>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ <reporting>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-project-info-reports-plugin</artifactId>
+ <version>3.1.2</version>
+ </plugin>
+ </plugins>
+ </reporting>
+ <profiles>
+ <profile>
+ <id>java8</id>
+ <activation>
+ <jdk>[1.8,)</jdk>
+ </activation>
+ <properties>
+ <javadoc.param>-Xdoclint:none</javadoc.param>
+ </properties>
+ </profile>
+ </profiles>
+</project>
diff --git a/core/src/main/java/com/google/common/truth/AbstractArraySubject.java b/core/src/main/java/com/google/common/truth/AbstractArraySubject.java
new file mode 100644
index 00000000..a0cd2fdc
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/AbstractArraySubject.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.truth.Fact.simpleFact;
+
+import java.lang.reflect.Array;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A common supertype for Array subjects, abstracting some common display and error infrastructure.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+abstract class AbstractArraySubject extends Subject {
+ private final Object actual;
+
+ AbstractArraySubject(
+ FailureMetadata metadata, @Nullable Object actual, @Nullable String typeDescription) {
+ super(metadata, actual, typeDescription);
+ this.actual = actual;
+ }
+
+ /** Fails if the array is not empty (i.e. {@code array.length > 0}). */
+ public final void isEmpty() {
+ if (length() > 0) {
+ failWithActual(simpleFact("expected to be empty"));
+ }
+ }
+
+ /** Fails if the array is empty (i.e. {@code array.length == 0}). */
+ public final void isNotEmpty() {
+ if (length() == 0) {
+ failWithoutActual(simpleFact("expected not to be empty"));
+ }
+ }
+
+ /**
+ * Fails if the array does not have the given length.
+ *
+ * @throws IllegalArgumentException if {@code length < 0}
+ */
+ public final void hasLength(int length) {
+ checkArgument(length >= 0, "length (%s) must be >= 0");
+ check("length").that(length()).isEqualTo(length);
+ }
+
+ private int length() {
+ return Array.getLength(actual);
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/ActualValueInference.java b/core/src/main/java/com/google/common/truth/ActualValueInference.java
new file mode 100644
index 00000000..5959bb60
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/ActualValueInference.java
@@ -0,0 +1,1520 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static java.lang.Thread.currentThread;
+
+import com.google.auto.value.AutoValue;
+import com.google.auto.value.AutoValue.CopyAnnotations;
+import com.google.common.annotations.GwtIncompatible;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.CheckReturnValue;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Map.Entry;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+/**
+ * Given the stack frame of a failing assertion, tries to describe what the user passed to {@code
+ * assertThat}.
+ *
+ * <p>For example, suppose that the test contains:
+ *
+ * <pre>{@code
+ * assertThat(logService.fetchLogMessages(startDate, endDate))
+ * .containsExactly(message1, message2)
+ * .inOrder();
+ * }</pre>
+ *
+ * If either {@code containsExactly} or {@code inOrder} fails, {@code ActualValueInference} reports
+ * (if the rest of the test method is simple enough to analyze easily) that the user passed {@code
+ * fetchLogMessages(...)}. This allows us to produce a failure message like:
+ *
+ * <pre>
+ * value of : fetchLogMessages(...)
+ * missing (1): message1
+ * ...
+ * </pre>
+ *
+ * {@code ActualValueInference} accomplishes this by examining the bytecode of the test. Naturally,
+ * this is all best-effort.
+ */
+@GwtIncompatible
+final class ActualValueInference {
+ /** <b>Call {@link Platform#inferDescription} rather than calling this directly.</b> */
+ static String describeActualValue(String className, String methodName, int lineNumber) {
+ InferenceClassVisitor visitor;
+ try {
+ // TODO(cpovirk): Verify that methodName is correct for constructors and static initializers.
+ visitor = new InferenceClassVisitor(methodName);
+ } catch (IllegalArgumentException theVersionOfAsmIsOlderThanWeRequire) {
+ // TODO(cpovirk): Consider what minimum version the class and method visitors really need.
+ // TODO(cpovirk): Log a warning?
+ return null;
+ }
+
+ ClassLoader loader =
+ firstNonNull(
+ currentThread().getContextClassLoader(), ActualValueInference.class.getClassLoader());
+ /*
+ * We're assuming that classes were loaded in a simple way. In principle, we could do better
+ * with java.lang.instrument.
+ */
+ InputStream stream = null;
+ try {
+ stream = loader.getResourceAsStream(className.replace('.', '/') + ".class");
+ // TODO(cpovirk): Disable inference if the bytecode version is newer than we've tested on?
+ new ClassReader(stream).accept(visitor, /*parsingOptions=*/ 0);
+ ImmutableSet<StackEntry> actualsAtLine = visitor.actualValueAtLine.build().get(lineNumber);
+ /*
+ * It's very unlikely that more than one assertion would happen on the same line _but with
+ * different root actual values_.
+ *
+ * That is, it's common to have:
+ * assertThat(list).containsExactly(...).inOrder();
+ *
+ * But it's not common to have, all on one line:
+ * assertThat(list).isEmpty(); assertThat(list2).containsExactly(...);
+ *
+ * In principle, we could try to distinguish further by looking at what assertion method
+ * failed (which our caller could pass us by looking higher on the stack). But it's hard to
+ * imagine that it would be worthwhile.
+ */
+ return actualsAtLine.size() == 1 ? getOnlyElement(actualsAtLine).description() : null;
+ } catch (IOException e) {
+ /*
+ * Likely "Class not found," perhaps from generated bytecode (or from StackTraceCleaner's
+ * pseudo-frames, which ideally ActualValueInference would tell it not to create).
+ */
+ // TODO(cpovirk): Log a warning?
+ return null;
+ } catch (SecurityException e) {
+ // Inside Google, some tests run under a security manager that forbids filesystem access.
+ // TODO(cpovirk): Log a warning?
+ return null;
+ } finally {
+ closeQuietly(stream);
+ }
+ }
+
+ /**
+ * An entry on the stack (or the local-variable table) with a {@linkplain InferredType type} and
+ * sometimes a description of {@linkplain DescribedEntry how the value was produced} or, as a
+ * special case, whether {@linkplain SubjectEntry the value is a Truth subject}.
+ */
+ abstract static class StackEntry {
+ abstract InferredType type();
+
+ // Each of these is overridden by a subclass:
+
+ boolean isSubject() {
+ return false;
+ }
+
+ StackEntry actualValue() {
+ throw new ClassCastException(getClass().getName());
+ }
+
+ String description() {
+ return null;
+ }
+ }
+
+ /** An entry that we know nothing about except for its type. */
+ @AutoValue
+ @CopyAnnotations
+ @GwtIncompatible
+ abstract static class OpaqueEntry extends StackEntry {
+ @Override
+ public final String toString() {
+ return "unknown";
+ }
+ }
+
+ private static StackEntry opaque(InferredType type) {
+ return new AutoValue_ActualValueInference_OpaqueEntry(type);
+ }
+
+ /**
+ * An entry that contains a description of how it was created. Currently, the only case in which
+ * we provide a description is when the value comes from a method call whose name looks
+ * "interesting."
+ */
+ @AutoValue
+ @CopyAnnotations
+ @GwtIncompatible
+ abstract static class DescribedEntry extends StackEntry {
+ @Override
+ abstract String description();
+
+ @Override
+ public final String toString() {
+ return description();
+ }
+ }
+
+ private static StackEntry described(InferredType type, String description) {
+ return new AutoValue_ActualValueInference_DescribedEntry(type, description);
+ }
+
+ /**
+ * An entry for a {@link Subject} (or a similar object derived with a {@code Subject}, like {@link
+ * Ordered}).
+ *
+ * <p>The entry contains the "root actual value" of the assertion. In an assertion like {@code
+ * assertThat(e).hasMessageThat().contains("foo")}, the root actual value is the {@code Throwable}
+ * {@code e}, even though the {@code contains} assertion operates on a string message.
+ */
+ @AutoValue
+ @CopyAnnotations
+ @GwtIncompatible
+ abstract static class SubjectEntry extends StackEntry {
+ @Override
+ abstract StackEntry actualValue();
+
+ @Override
+ final boolean isSubject() {
+ return true;
+ }
+
+ @Override
+ public final String toString() {
+ return String.format("subjectFor(%s)", actualValue());
+ }
+ }
+
+ private static StackEntry subjectFor(InferredType type, StackEntry actual) {
+ return new AutoValue_ActualValueInference_SubjectEntry(type, actual);
+ }
+
+ private static final class InferenceMethodVisitor extends MethodVisitor {
+ private boolean used = false;
+ private final ArrayList<StackEntry> localVariableSlots;
+ private final ArrayList<StackEntry> operandStack = new ArrayList<>();
+ private FrameInfo previousFrame;
+ /** For debugging purpose. */
+ private final String methodSignature;
+
+ /**
+ * The ASM labels that we've seen so far, which we use to look up the closest line number for
+ * each assertion.
+ */
+ private final ImmutableList.Builder<Label> labelsSeen = ImmutableList.builder();
+
+ /**
+ * The mapping from label to line number.
+ *
+ * <p>I had hoped that we didn't need this: In the {@code .class} files I looked at, {@code
+ * visitLineNumber} calls were interleaved with the actual instructions. (I even have evidence
+ * that the current implementation visits labels and line numbers together: See Label.accept.)
+ * If that were guaranteed, then we could identify the line number for each assertion just by
+ * looking at which {@code visitLineNumber} call we'd seen most recently. However, that
+ * <i>doesn't</i> appear to be guaranteed, so we store this mapping and then join it with the
+ * labels at the end.
+ *
+ * <p>I would expect to be able to use a map here. But I'm seeing multiple line numbers for the
+ * same label in some Kotlin code.
+ */
+ private final ImmutableSetMultimap.Builder<Label, Integer> lineNumbersAtLabel =
+ ImmutableSetMultimap.builder();
+
+ /**
+ * The mapping that indexes every root actual value by the full list of labels we'd visited
+ * before we visited it.
+ */
+ private final ImmutableSetMultimap.Builder<ImmutableList<Label>, StackEntry>
+ actualValueAtLocation = ImmutableSetMultimap.builder();
+
+ /** Set to {@code true} whenever a method permits multiple execution paths. */
+ private boolean seenJump;
+
+ /**
+ * The output of this process: a mapping from line number to the root actual values with
+ * assertions on that line. This builder is potentially shared across multiple method visitors
+ * for the same class visitor.
+ */
+ private final ImmutableSetMultimap.Builder<Integer, StackEntry> actualValueAtLine;
+
+ InferenceMethodVisitor(
+ int access,
+ String owner,
+ String name,
+ String methodDescriptor,
+ ImmutableSetMultimap.Builder<Integer, StackEntry> actualValueAtLine) {
+ super(Opcodes.ASM8);
+ localVariableSlots = createInitialLocalVariableSlots(access, owner, name, methodDescriptor);
+ previousFrame =
+ FrameInfo.create(
+ ImmutableList.copyOf(localVariableSlots), ImmutableList.<StackEntry>of());
+ this.methodSignature = owner + "." + name + methodDescriptor;
+ this.actualValueAtLine = actualValueAtLine;
+ }
+
+ @Override
+ public void visitCode() {
+ checkState(!used, "Cannot reuse this method visitor.");
+ used = true;
+ super.visitCode();
+ }
+
+ @Override
+ public void visitEnd() {
+ if (seenJump) {
+ /*
+ * If there are multiple paths through a method, we'd have to examine them all and make sure
+ * that the values still match up. We could try someday, but it's hard.
+ */
+ super.visitEnd();
+ return;
+ }
+ ImmutableSetMultimap<Label, Integer> lineNumbersAtLabel = this.lineNumbersAtLabel.build();
+ for (Entry<ImmutableList<Label>, StackEntry> e : actualValueAtLocation.build().entries()) {
+ for (int lineNumber : lineNumbers(e.getKey(), lineNumbersAtLabel)) {
+ actualValueAtLine.put(lineNumber, e.getValue());
+ }
+ }
+ super.visitEnd();
+ }
+
+ private static ImmutableSet<Integer> lineNumbers(
+ ImmutableList<Label> labels, ImmutableSetMultimap<Label, Integer> lineNumbersAtLabel) {
+ for (Label label : labels.reverse()) {
+ if (lineNumbersAtLabel.containsKey(label)) {
+ return lineNumbersAtLabel.get(label);
+ }
+ }
+ return ImmutableSet.of();
+ }
+
+ @Override
+ public void visitLineNumber(int line, Label start) {
+ lineNumbersAtLabel.put(start, line);
+ super.visitLineNumber(line, start);
+ }
+
+ @Override
+ public void visitLabel(Label label) {
+ labelsSeen.add(label);
+ super.visitLabel(label);
+ }
+
+ /** Returns the entry for the operand at the specified offset. 0 means the top of the stack. */
+ private StackEntry getOperandFromTop(int offsetFromTop) {
+ int index = operandStack.size() - 1 - offsetFromTop;
+ checkState(
+ index >= 0,
+ "Invalid offset %s in the list of size %s. The current method is %s",
+ offsetFromTop,
+ operandStack.size(),
+ methodSignature);
+ return operandStack.get(index);
+ }
+
+ @Override
+ public void visitInsn(int opcode) {
+ switch (opcode) {
+ case Opcodes.NOP:
+ case Opcodes.INEG:
+ case Opcodes.LNEG:
+ case Opcodes.FNEG:
+ case Opcodes.DNEG:
+ case Opcodes.I2B:
+ case Opcodes.I2C:
+ case Opcodes.I2S:
+ case Opcodes.RETURN:
+ break;
+ case Opcodes.ACONST_NULL:
+ push(InferredType.NULL);
+ break;
+ case Opcodes.ICONST_M1:
+ case Opcodes.ICONST_0:
+ case Opcodes.ICONST_1:
+ case Opcodes.ICONST_2:
+ case Opcodes.ICONST_3:
+ case Opcodes.ICONST_4:
+ case Opcodes.ICONST_5:
+ push(InferredType.INT);
+ break;
+ case Opcodes.LCONST_0:
+ case Opcodes.LCONST_1:
+ push(InferredType.LONG);
+ push(InferredType.TOP);
+ break;
+ case Opcodes.FCONST_0:
+ case Opcodes.FCONST_1:
+ case Opcodes.FCONST_2:
+ push(InferredType.FLOAT);
+ break;
+ case Opcodes.DCONST_0:
+ case Opcodes.DCONST_1:
+ push(InferredType.DOUBLE);
+ push(InferredType.TOP);
+ break;
+ case Opcodes.IALOAD:
+ case Opcodes.BALOAD:
+ case Opcodes.CALOAD:
+ case Opcodes.SALOAD:
+ pop(2);
+ push(InferredType.INT);
+ break;
+ case Opcodes.LALOAD:
+ case Opcodes.D2L:
+ pop(2);
+ push(InferredType.LONG);
+ push(InferredType.TOP);
+ break;
+ case Opcodes.DALOAD:
+ case Opcodes.L2D:
+ pop(2);
+ push(InferredType.DOUBLE);
+ push(InferredType.TOP);
+ break;
+ case Opcodes.AALOAD:
+ InferredType arrayType = pop(2).type();
+ InferredType elementType = arrayType.getElementTypeIfArrayOrThrow();
+ push(elementType);
+ break;
+ case Opcodes.IASTORE:
+ case Opcodes.BASTORE:
+ case Opcodes.CASTORE:
+ case Opcodes.SASTORE:
+ case Opcodes.FASTORE:
+ case Opcodes.AASTORE:
+ pop(3);
+ break;
+ case Opcodes.LASTORE:
+ case Opcodes.DASTORE:
+ pop(4);
+ break;
+ case Opcodes.POP:
+ case Opcodes.IRETURN:
+ case Opcodes.FRETURN:
+ case Opcodes.ARETURN:
+ case Opcodes.ATHROW:
+ case Opcodes.MONITORENTER:
+ case Opcodes.MONITOREXIT:
+ pop();
+ break;
+ case Opcodes.POP2:
+ case Opcodes.LRETURN:
+ case Opcodes.DRETURN:
+ pop(2);
+ break;
+ case Opcodes.DUP:
+ push(top());
+ break;
+ case Opcodes.DUP_X1:
+ {
+ StackEntry top = pop();
+ StackEntry next = pop();
+ push(top);
+ push(next);
+ push(top);
+ break;
+ }
+ case Opcodes.DUP_X2:
+ {
+ StackEntry top = pop();
+ StackEntry next = pop();
+ StackEntry bottom = pop();
+ push(top);
+ push(bottom);
+ push(next);
+ push(top);
+ break;
+ }
+ case Opcodes.DUP2:
+ {
+ StackEntry top = pop();
+ StackEntry next = pop();
+ push(next);
+ push(top);
+ push(next);
+ push(top);
+ break;
+ }
+ case Opcodes.DUP2_X1:
+ {
+ StackEntry top = pop();
+ StackEntry next = pop();
+ StackEntry bottom = pop();
+ push(next);
+ push(top);
+ push(bottom);
+ push(next);
+ push(top);
+ break;
+ }
+ case Opcodes.DUP2_X2:
+ {
+ StackEntry t1 = pop();
+ StackEntry t2 = pop();
+ StackEntry t3 = pop();
+ StackEntry t4 = pop();
+ push(t2);
+ push(t1);
+ push(t4);
+ push(t3);
+ push(t2);
+ push(t1);
+ break;
+ }
+ case Opcodes.SWAP:
+ {
+ StackEntry top = pop();
+ StackEntry next = pop();
+ push(top);
+ push(next);
+ break;
+ }
+ case Opcodes.IADD:
+ case Opcodes.ISUB:
+ case Opcodes.IMUL:
+ case Opcodes.IDIV:
+ case Opcodes.IREM:
+ case Opcodes.ISHL:
+ case Opcodes.ISHR:
+ case Opcodes.IUSHR:
+ case Opcodes.IAND:
+ case Opcodes.IOR:
+ case Opcodes.IXOR:
+ case Opcodes.L2I:
+ case Opcodes.D2I:
+ case Opcodes.FCMPL:
+ case Opcodes.FCMPG:
+ pop(2);
+ push(InferredType.INT);
+ break;
+
+ case Opcodes.LADD:
+ case Opcodes.LSUB:
+ case Opcodes.LMUL:
+ case Opcodes.LDIV:
+ case Opcodes.LREM:
+ case Opcodes.LAND:
+ case Opcodes.LOR:
+ case Opcodes.LXOR:
+ pop(4);
+ push(InferredType.LONG);
+ push(InferredType.TOP);
+ break;
+
+ case Opcodes.LSHL:
+ case Opcodes.LSHR:
+ case Opcodes.LUSHR:
+ pop(3);
+ push(InferredType.LONG);
+ push(InferredType.TOP);
+ break;
+ case Opcodes.I2L:
+ case Opcodes.F2L:
+ pop();
+ push(InferredType.LONG);
+ push(InferredType.TOP);
+ break;
+ case Opcodes.I2F:
+ pop();
+ push(InferredType.FLOAT);
+ break;
+
+ case Opcodes.LCMP:
+ case Opcodes.DCMPG:
+ case Opcodes.DCMPL:
+ pop(4);
+ push(InferredType.INT);
+ break;
+
+ case Opcodes.I2D:
+ case Opcodes.F2D:
+ pop();
+ push(InferredType.DOUBLE);
+ push(InferredType.TOP);
+ break;
+ case Opcodes.F2I:
+ case Opcodes.ARRAYLENGTH:
+ pop();
+ push(InferredType.INT);
+ break;
+ case Opcodes.FALOAD:
+ case Opcodes.FADD:
+ case Opcodes.FSUB:
+ case Opcodes.FMUL:
+ case Opcodes.FDIV:
+ case Opcodes.FREM:
+ case Opcodes.L2F:
+ case Opcodes.D2F:
+ pop(2);
+ push(InferredType.FLOAT);
+ break;
+
+ case Opcodes.DADD:
+ case Opcodes.DSUB:
+ case Opcodes.DMUL:
+ case Opcodes.DDIV:
+ case Opcodes.DREM:
+ pop(4);
+ push(InferredType.DOUBLE);
+ push(InferredType.TOP);
+ break;
+ default:
+ throw new RuntimeException("Unhandled opcode " + opcode);
+ }
+ super.visitInsn(opcode);
+ }
+
+ @Override
+ public void visitIntInsn(int opcode, int operand) {
+ switch (opcode) {
+ case Opcodes.BIPUSH:
+ case Opcodes.SIPUSH:
+ push(InferredType.INT);
+ break;
+ case Opcodes.NEWARRAY:
+ pop();
+ switch (operand) {
+ case Opcodes.T_BOOLEAN:
+ pushDescriptor("[Z");
+ break;
+ case Opcodes.T_CHAR:
+ pushDescriptor("[C");
+ break;
+ case Opcodes.T_FLOAT:
+ pushDescriptor("[F");
+ break;
+ case Opcodes.T_DOUBLE:
+ pushDescriptor("[D");
+ break;
+ case Opcodes.T_BYTE:
+ pushDescriptor("[B");
+ break;
+ case Opcodes.T_SHORT:
+ pushDescriptor("[S");
+ break;
+ case Opcodes.T_INT:
+ pushDescriptor("[I");
+ break;
+ case Opcodes.T_LONG:
+ pushDescriptor("[J");
+ break;
+ default:
+ throw new RuntimeException("Unhandled operand value: " + operand);
+ }
+ break;
+ default:
+ throw new RuntimeException("Unhandled opcode " + opcode);
+ }
+ super.visitIntInsn(opcode, operand);
+ }
+
+ @Override
+ public void visitVarInsn(int opcode, int var) {
+ switch (opcode) {
+ case Opcodes.ILOAD:
+ push(InferredType.INT);
+ break;
+ case Opcodes.LLOAD:
+ push(InferredType.LONG);
+ push(InferredType.TOP);
+ break;
+ case Opcodes.FLOAD:
+ push(InferredType.FLOAT);
+ break;
+ case Opcodes.DLOAD:
+ push(InferredType.DOUBLE);
+ push(InferredType.TOP);
+ break;
+ case Opcodes.ALOAD:
+ push(getLocalVariable(var));
+ break;
+ case Opcodes.ISTORE:
+ case Opcodes.FSTORE:
+ case Opcodes.ASTORE:
+ {
+ StackEntry entry = pop();
+ setLocalVariable(var, entry);
+ break;
+ }
+ case Opcodes.LSTORE:
+ case Opcodes.DSTORE:
+ {
+ StackEntry entry = pop(2);
+ setLocalVariable(var, entry);
+ setLocalVariable(var + 1, opaque(InferredType.TOP));
+ break;
+ }
+ case Opcodes.RET:
+ throw new RuntimeException("The instruction RET is not supported");
+ default:
+ throw new RuntimeException("Unhandled opcode " + opcode);
+ }
+ super.visitVarInsn(opcode, var);
+ }
+
+ @Override
+ public void visitTypeInsn(int opcode, String type) {
+ String descriptor = convertToDescriptor(type);
+ switch (opcode) {
+ case Opcodes.NEW:
+ // This should be UNINITIALIZED(label). Okay for type inference.
+ pushDescriptor(descriptor);
+ break;
+ case Opcodes.ANEWARRAY:
+ pop();
+ pushDescriptor('[' + descriptor);
+ break;
+ case Opcodes.CHECKCAST:
+ pop();
+ pushDescriptor(descriptor);
+ break;
+ case Opcodes.INSTANCEOF:
+ pop();
+ push(InferredType.INT);
+ break;
+ default:
+ throw new RuntimeException("Unhandled opcode " + opcode);
+ }
+ super.visitTypeInsn(opcode, type);
+ }
+
+ @Override
+ public void visitFieldInsn(int opcode, String owner, String name, String desc) {
+ switch (opcode) {
+ case Opcodes.GETSTATIC:
+ pushDescriptor(desc);
+ break;
+ case Opcodes.PUTSTATIC:
+ popDescriptor(desc);
+ break;
+ case Opcodes.GETFIELD:
+ pop();
+ pushDescriptor(desc);
+ break;
+ case Opcodes.PUTFIELD:
+ popDescriptor(desc);
+ pop();
+ break;
+ default:
+ throw new RuntimeException(
+ "Unhandled opcode "
+ + opcode
+ + ", owner="
+ + owner
+ + ", name="
+ + name
+ + ", desc"
+ + desc);
+ }
+ super.visitFieldInsn(opcode, owner, name, desc);
+ }
+
+ @Override
+ public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+ if (opcode == Opcodes.INVOKESPECIAL && "<init>".equals(name)) {
+ int argumentSize = (Type.getArgumentsAndReturnSizes(desc) >> 2);
+ InferredType receiverType = getOperandFromTop(argumentSize - 1).type();
+ if (receiverType.isUninitialized()) {
+ InferredType realType = InferredType.create('L' + owner + ';');
+ replaceUninitializedTypeInStack(receiverType, realType);
+ }
+ }
+ switch (opcode) {
+ case Opcodes.INVOKESPECIAL:
+ case Opcodes.INVOKEVIRTUAL:
+ case Opcodes.INVOKESTATIC:
+ case Opcodes.INVOKEINTERFACE:
+ Invocation.Builder invocation = Invocation.builder(name);
+
+ if (isThatOrAssertThat(owner, name)) {
+ invocation.setActualValue(getOperandFromTop(0));
+ } else if (isBoxing(owner, name, desc)) {
+ invocation.setBoxingInput(
+ // double and long are represented by a TOP with the "real" value under it.
+ getOperandFromTop(0).type() == InferredType.TOP
+ ? getOperandFromTop(1)
+ : getOperandFromTop(0));
+ }
+
+ popDescriptor(desc);
+
+ if (opcode != Opcodes.INVOKESTATIC) {
+ invocation.setReceiver(pop());
+ }
+
+ pushDescriptorAndMaybeProcessMethodCall(desc, invocation.build());
+ break;
+ default:
+ throw new RuntimeException(
+ String.format(
+ "Unhandled opcode %s, owner=%s, name=%s, desc=%s, itf=%s",
+ opcode, owner, name, desc, itf));
+ }
+ super.visitMethodInsn(opcode, owner, name, desc, itf);
+ }
+
+ @Override
+ public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) {
+ popDescriptor(desc);
+ pushDescriptor(desc);
+ super.visitInvokeDynamicInsn(name, desc, bsm, bsmArgs);
+ }
+
+ @Override
+ public void visitJumpInsn(int opcode, Label label) {
+ seenJump = true;
+ switch (opcode) {
+ case Opcodes.IFEQ:
+ case Opcodes.IFNE:
+ case Opcodes.IFLT:
+ case Opcodes.IFGE:
+ case Opcodes.IFGT:
+ case Opcodes.IFLE:
+ pop();
+ break;
+ case Opcodes.IF_ICMPEQ:
+ case Opcodes.IF_ICMPNE:
+ case Opcodes.IF_ICMPLT:
+ case Opcodes.IF_ICMPGE:
+ case Opcodes.IF_ICMPGT:
+ case Opcodes.IF_ICMPLE:
+ case Opcodes.IF_ACMPEQ:
+ case Opcodes.IF_ACMPNE:
+ pop(2);
+ break;
+ case Opcodes.GOTO:
+ break;
+ case Opcodes.JSR:
+ throw new RuntimeException("The JSR instruction is not supported.");
+ case Opcodes.IFNULL:
+ case Opcodes.IFNONNULL:
+ pop(1);
+ break;
+ default:
+ throw new RuntimeException("Unhandled opcode " + opcode);
+ }
+ super.visitJumpInsn(opcode, label);
+ }
+
+ @Override
+ public void visitLdcInsn(Object cst) {
+ if (cst instanceof Integer) {
+ push(InferredType.INT);
+ } else if (cst instanceof Float) {
+ push(InferredType.FLOAT);
+ } else if (cst instanceof Long) {
+ push(InferredType.LONG);
+ push(InferredType.TOP);
+ } else if (cst instanceof Double) {
+ push(InferredType.DOUBLE);
+ push(InferredType.TOP);
+ } else if (cst instanceof String) {
+ pushDescriptor("Ljava/lang/String;");
+ } else if (cst instanceof Type) {
+ pushDescriptor(((Type) cst).getDescriptor());
+ } else if (cst instanceof Handle) {
+ pushDescriptor("Ljava/lang/invoke/MethodHandle;");
+ } else {
+ throw new RuntimeException("Cannot handle constant " + cst + " for LDC instruction");
+ }
+ super.visitLdcInsn(cst);
+ }
+
+ @Override
+ public void visitIincInsn(int var, int increment) {
+ setLocalVariable(var, opaque(InferredType.INT));
+ super.visitIincInsn(var, increment);
+ }
+
+ @Override
+ public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) {
+ seenJump = true;
+ pop();
+ super.visitTableSwitchInsn(min, max, dflt, labels);
+ }
+
+ @Override
+ public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
+ seenJump = true;
+ pop();
+ super.visitLookupSwitchInsn(dflt, keys, labels);
+ }
+
+ @Override
+ public void visitTryCatchBlock(Label start, Label end, Label handler, String type) {
+ /*
+ * Inference already fails for at least some try-catch blocks, apparently because of the extra
+ * frames they create. Still, let's disable inference explicitly.
+ */
+ seenJump = true;
+ super.visitTryCatchBlock(start, end, handler, type);
+ }
+
+ @Override
+ public void visitMultiANewArrayInsn(String desc, int dims) {
+ pop(dims);
+ pushDescriptor(desc);
+ super.visitMultiANewArrayInsn(desc, dims);
+ }
+
+ @Override
+ public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) {
+ switch (type) {
+ case Opcodes.F_NEW:
+ // Expanded form.
+ previousFrame =
+ FrameInfo.create(
+ convertTypesInStackMapFrame(nLocal, local),
+ convertTypesInStackMapFrame(nStack, stack));
+ break;
+ case Opcodes.F_SAME:
+ // This frame type indicates that the frame has exactly the same local variables as the
+ // previous frame and that the operand stack is empty.
+ previousFrame = FrameInfo.create(previousFrame.locals(), ImmutableList.<StackEntry>of());
+ break;
+ case Opcodes.F_SAME1:
+ // This frame type indicates that the frame has exactly the same local variables as the
+ // previous frame and that the operand stack has one entry.
+ previousFrame =
+ FrameInfo.create(previousFrame.locals(), convertTypesInStackMapFrame(nStack, stack));
+ break;
+ case Opcodes.F_APPEND:
+ // This frame type indicates that the frame has the same locals as the previous frame
+ // except that k additional locals are defined, and that the operand stack is empty.
+ previousFrame =
+ FrameInfo.create(
+ appendArrayToList(previousFrame.locals(), nLocal, local),
+ ImmutableList.<StackEntry>of());
+ break;
+ case Opcodes.F_CHOP:
+ // This frame type indicates that the frame has the same local variables as the previous
+ // frame except that the last k local variables are absent, and that the operand stack is
+ // empty.
+ previousFrame =
+ FrameInfo.create(
+ removeBackFromList(previousFrame.locals(), nLocal),
+ ImmutableList.<StackEntry>of());
+ break;
+ case Opcodes.F_FULL:
+ previousFrame =
+ FrameInfo.create(
+ convertTypesInStackMapFrame(nLocal, local),
+ convertTypesInStackMapFrame(nStack, stack));
+ break;
+ default:
+ // continue below
+ }
+ // Update types for operand stack and local variables.
+ operandStack.clear();
+ operandStack.addAll(previousFrame.stack());
+ localVariableSlots.clear();
+ localVariableSlots.addAll(previousFrame.locals());
+ super.visitFrame(type, nLocal, local, nStack, stack);
+ }
+
+ private static String convertToDescriptor(String type) {
+ return (type.length() > 1 && type.charAt(0) != '[') ? 'L' + type + ';' : type;
+ }
+
+ private void push(InferredType type) {
+ push(opaque(type));
+ }
+
+ private void push(StackEntry entry) {
+ operandStack.add(entry);
+ }
+
+ private void replaceUninitializedTypeInStack(InferredType oldType, InferredType newType) {
+ checkArgument(oldType.isUninitialized(), "The old type is NOT uninitialized. %s", oldType);
+ for (int i = 0, size = operandStack.size(); i < size; ++i) {
+ InferredType type = operandStack.get(i).type();
+ if (type.equals(oldType)) {
+ operandStack.set(i, opaque(newType));
+ }
+ }
+ }
+
+ private void pushDescriptor(String desc) {
+ pushDescriptorAndMaybeProcessMethodCall(desc, /*invocation=*/ null);
+ }
+
+ /**
+ * Pushes entries onto the stack for the given arguments, and, if the descriptor is for a method
+ * call, records the assertion made by that call (if any).
+ *
+ * <p>If the descriptor is for a call, this method not only records the assertion made by it (if
+ * any) but also examines its parameters to generate more detailed stack entries.
+ *
+ * @param desc the descriptor of the type to be added to the stack (or the descriptor of the
+ * method whose return value is to be added to the stack)
+ * @param invocation the method invocation being visited, or {@code null} if a non-method
+ * descriptor is being visited
+ */
+ private void pushDescriptorAndMaybeProcessMethodCall(String desc, Invocation invocation) {
+ if (invocation != null && invocation.isOnSubjectInstance()) {
+ actualValueAtLocation.put(labelsSeen.build(), invocation.receiver().actualValue());
+ }
+
+ boolean hasParams = invocation != null && (Type.getArgumentsAndReturnSizes(desc) >> 2) > 1;
+ int index = desc.charAt(0) == '(' ? desc.indexOf(')') + 1 : 0;
+ switch (desc.charAt(index)) {
+ case 'V':
+ return;
+ case 'Z':
+ case 'C':
+ case 'B':
+ case 'S':
+ case 'I':
+ pushMaybeDescribed(InferredType.INT, invocation, hasParams);
+ break;
+ case 'F':
+ pushMaybeDescribed(InferredType.FLOAT, invocation, hasParams);
+ break;
+ case 'D':
+ pushMaybeDescribed(InferredType.DOUBLE, invocation, hasParams);
+ push(InferredType.TOP);
+ break;
+ case 'J':
+ pushMaybeDescribed(InferredType.LONG, invocation, hasParams);
+ push(InferredType.TOP);
+ break;
+ case 'L':
+ case '[':
+ pushMaybeDescribed(InferredType.create(desc.substring(index)), invocation, hasParams);
+ break;
+ default:
+ throw new RuntimeException("Unhandled type: " + desc);
+ }
+ }
+
+ private void pushMaybeDescribed(InferredType type, Invocation invocation, boolean hasParams) {
+ push(invocation == null ? opaque(type) : invocation.deriveEntry(type, hasParams));
+ }
+
+ @CanIgnoreReturnValue
+ private StackEntry pop() {
+ return pop(1);
+ }
+
+ /** Pop elements from the end of the operand stack, and return the last popped element. */
+ @CanIgnoreReturnValue
+ private StackEntry pop(int count) {
+ checkArgument(
+ count >= 1, "The count should be at least one: %s (In %s)", count, methodSignature);
+ checkState(
+ operandStack.size() >= count,
+ "There are no enough elements in the stack. count=%s, stack=%s (In %s)",
+ count,
+ operandStack,
+ methodSignature);
+ int expectedLastIndex = operandStack.size() - count - 1;
+ StackEntry lastPopped = null;
+ for (int i = operandStack.size() - 1; i > expectedLastIndex; --i) {
+ lastPopped = operandStack.remove(i);
+ }
+ return lastPopped;
+ }
+
+ private void popDescriptor(String desc) {
+ char c = desc.charAt(0);
+ switch (c) {
+ case '(':
+ int argumentSize = (Type.getArgumentsAndReturnSizes(desc) >> 2) - 1;
+ if (argumentSize > 0) {
+ pop(argumentSize);
+ }
+ break;
+ case 'J':
+ case 'D':
+ pop(2);
+ break;
+ default:
+ pop(1);
+ break;
+ }
+ }
+
+ private StackEntry getLocalVariable(int index) {
+ checkState(
+ index < localVariableSlots.size(),
+ "Cannot find type for var %s in method %s",
+ index,
+ methodSignature);
+ return localVariableSlots.get(index);
+ }
+
+ private void setLocalVariable(int index, StackEntry entry) {
+ while (localVariableSlots.size() <= index) {
+ localVariableSlots.add(opaque(InferredType.TOP));
+ }
+ localVariableSlots.set(index, entry);
+ }
+
+ private StackEntry top() {
+ return operandStack.get(operandStack.size() - 1);
+ }
+
+ /**
+ * Create the slots for local variables at the very beginning of the method with the information
+ * of the declaring class and the method descriptor.
+ */
+ private static ArrayList<StackEntry> createInitialLocalVariableSlots(
+ int access, String ownerClass, String methodName, String methodDescriptor) {
+ ArrayList<StackEntry> entries = new ArrayList<>();
+
+ if (!isStatic(access)) {
+ // Instance method, and this is the receiver
+ entries.add(opaque(InferredType.create(convertToDescriptor(ownerClass))));
+ }
+ Type[] argumentTypes = Type.getArgumentTypes(methodDescriptor);
+ for (Type argumentType : argumentTypes) {
+ switch (argumentType.getSort()) {
+ case Type.BOOLEAN:
+ case Type.BYTE:
+ case Type.CHAR:
+ case Type.SHORT:
+ case Type.INT:
+ entries.add(opaque(InferredType.INT));
+ break;
+ case Type.FLOAT:
+ entries.add(opaque(InferredType.FLOAT));
+ break;
+ case Type.LONG:
+ entries.add(opaque(InferredType.LONG));
+ entries.add(opaque(InferredType.TOP));
+ break;
+ case Type.DOUBLE:
+ entries.add(opaque(InferredType.DOUBLE));
+ entries.add(opaque(InferredType.TOP));
+ break;
+ case Type.ARRAY:
+ case Type.OBJECT:
+ entries.add(opaque(InferredType.create(argumentType.getDescriptor())));
+ break;
+ default:
+ throw new RuntimeException(
+ "Unhandled argument type: "
+ + argumentType
+ + " in "
+ + ownerClass
+ + "."
+ + methodName
+ + methodDescriptor);
+ }
+ }
+ return entries;
+ }
+
+ private static ImmutableList<StackEntry> removeBackFromList(
+ ImmutableList<StackEntry> list, int countToRemove) {
+ int origSize = list.size();
+ int index = origSize - 1;
+
+ while (index >= 0 && countToRemove > 0) {
+ InferredType type = list.get(index).type();
+ if (type.equals(InferredType.TOP)
+ && index > 0
+ && list.get(index - 1).type().isCategory2()) {
+ --index; // A category 2 takes two slots.
+ }
+ --index; // Eat this local variable.
+ --countToRemove;
+ }
+ checkState(
+ countToRemove == 0,
+ "countToRemove is %s but not 0. index=%s, list=%s",
+ countToRemove,
+ index,
+ list);
+ return list.subList(0, index + 1);
+ }
+
+ private ImmutableList<StackEntry> appendArrayToList(
+ ImmutableList<StackEntry> list, int size, Object[] array) {
+ ImmutableList.Builder<StackEntry> builder = ImmutableList.builder();
+ builder.addAll(list);
+ for (int i = 0; i < size; ++i) {
+ InferredType type = convertTypeInStackMapFrame(array[i]);
+ builder.add(opaque(type));
+ if (type.isCategory2()) {
+ builder.add(opaque(InferredType.TOP));
+ }
+ }
+ return builder.build();
+ }
+
+ /** Convert the type in stack map frame to inference type. */
+ private InferredType convertTypeInStackMapFrame(Object typeInStackMapFrame) {
+ if (typeInStackMapFrame == Opcodes.TOP) {
+ return InferredType.TOP;
+ } else if (typeInStackMapFrame == Opcodes.INTEGER) {
+ return InferredType.INT;
+ } else if (typeInStackMapFrame == Opcodes.FLOAT) {
+ return InferredType.FLOAT;
+ } else if (typeInStackMapFrame == Opcodes.DOUBLE) {
+ return InferredType.DOUBLE;
+ } else if (typeInStackMapFrame == Opcodes.LONG) {
+ return InferredType.LONG;
+ } else if (typeInStackMapFrame == Opcodes.NULL) {
+ return InferredType.NULL;
+ } else if (typeInStackMapFrame == Opcodes.UNINITIALIZED_THIS) {
+ return InferredType.UNINITIALIZED_THIS;
+ } else if (typeInStackMapFrame instanceof String) {
+ String referenceTypeName = (String) typeInStackMapFrame;
+ if (referenceTypeName.charAt(0) == '[') {
+ return InferredType.create(referenceTypeName);
+ } else {
+ return InferredType.create('L' + referenceTypeName + ';');
+ }
+ } else if (typeInStackMapFrame instanceof Label) {
+ return InferredType.UNINITIALIZED;
+ } else {
+ throw new RuntimeException(
+ "Cannot reach here. Unhandled element: value="
+ + typeInStackMapFrame
+ + ", class="
+ + typeInStackMapFrame.getClass()
+ + ". The current method being desugared is "
+ + methodSignature);
+ }
+ }
+
+ private ImmutableList<StackEntry> convertTypesInStackMapFrame(int size, Object[] array) {
+ ImmutableList.Builder<StackEntry> builder = ImmutableList.builder();
+ for (int i = 0; i < size; ++i) {
+ InferredType type = convertTypeInStackMapFrame(array[i]);
+ builder.add(opaque(type));
+ if (type.isCategory2()) {
+ builder.add(opaque(InferredType.TOP));
+ }
+ }
+ return builder.build();
+ }
+ }
+
+ /** A value class to represent a frame. */
+ @AutoValue
+ @CopyAnnotations
+ @GwtIncompatible
+ abstract static class FrameInfo {
+
+ static FrameInfo create(ImmutableList<StackEntry> locals, ImmutableList<StackEntry> stack) {
+ return new AutoValue_ActualValueInference_FrameInfo(locals, stack);
+ }
+
+ abstract ImmutableList<StackEntry> locals();
+
+ abstract ImmutableList<StackEntry> stack();
+ }
+
+ /** A method invocation. */
+ @AutoValue
+ @CopyAnnotations
+ @GwtIncompatible
+ abstract static class Invocation {
+ static Builder builder(String name) {
+ return new AutoValue_ActualValueInference_Invocation.Builder().setName(name);
+ }
+
+ /** The receiver of this call, if it is an instance call. */
+ @Nullable
+ abstract StackEntry receiver();
+
+ /** The value being passed to this call if it is an {@code assertThat} or {@code that} call. */
+ @Nullable
+ abstract StackEntry actualValue();
+
+ /**
+ * The value being passed to this call if it is a boxing call (e.g., {@code Integer.valueOf}).
+ */
+ @Nullable
+ abstract StackEntry boxingInput();
+
+ abstract String name();
+
+ final StackEntry deriveEntry(InferredType type, boolean hasParams) {
+ if (boxingInput() != null && boxingInput().description() != null) {
+ return described(type, boxingInput().description());
+ } else if (actualValue() != null) {
+ return subjectFor(type, actualValue());
+ } else if (isOnSubjectInstance()) {
+ return subjectFor(type, receiver().actualValue());
+ } else if (BORING_NAMES.contains(name())) {
+ /*
+ * TODO(cpovirk): For no-arg instance methods like get(), return "foo.get()," where "foo" is
+ * the description we had for the receiver (if any).
+ */
+ return opaque(type);
+ } else {
+ return described(type, name() + (hasParams ? "(...)" : "()"));
+ }
+ }
+
+ final boolean isOnSubjectInstance() {
+ return receiver() != null && receiver().isSubject();
+ }
+
+ @AutoValue.Builder
+ @CanIgnoreReturnValue
+ abstract static class Builder {
+ abstract Builder setReceiver(StackEntry receiver);
+
+ abstract Builder setActualValue(StackEntry actualValue);
+
+ abstract Builder setBoxingInput(StackEntry boxingInput);
+
+ abstract Builder setName(String name);
+
+ @CheckReturnValue
+ abstract Invocation build();
+ }
+ }
+
+ /** This is the type used for type inference. */
+ @AutoValue
+ @CopyAnnotations
+ @GwtIncompatible
+ abstract static class InferredType {
+
+ static final String UNINITIALIZED_PREFIX = "UNINIT@";
+
+ static final InferredType BOOLEAN = new AutoValue_ActualValueInference_InferredType("Z");
+ static final InferredType BYTE = new AutoValue_ActualValueInference_InferredType("B");
+ static final InferredType INT = new AutoValue_ActualValueInference_InferredType("I");
+ static final InferredType FLOAT = new AutoValue_ActualValueInference_InferredType("F");
+ static final InferredType LONG = new AutoValue_ActualValueInference_InferredType("J");
+ static final InferredType DOUBLE = new AutoValue_ActualValueInference_InferredType("D");
+ /** Not a real value. */
+ static final InferredType TOP = new AutoValue_ActualValueInference_InferredType("TOP");
+ /** The value NULL */
+ static final InferredType NULL = new AutoValue_ActualValueInference_InferredType("NULL");
+
+ static final InferredType UNINITIALIZED_THIS =
+ new AutoValue_ActualValueInference_InferredType("UNINITIALIZED_THIS");
+
+ static final InferredType UNINITIALIZED =
+ new AutoValue_ActualValueInference_InferredType(UNINITIALIZED_PREFIX);
+
+ /** Create a type for a value. */
+ static InferredType create(String descriptor) {
+ if (UNINITIALIZED_PREFIX.equals(descriptor)) {
+ return UNINITIALIZED;
+ }
+ char firstChar = descriptor.charAt(0);
+ if (firstChar == 'L' || firstChar == '[') {
+ // Reference, array.
+ return new AutoValue_ActualValueInference_InferredType(descriptor);
+ }
+ switch (descriptor) {
+ case "Z":
+ return BOOLEAN;
+ case "B":
+ return BYTE;
+ case "I":
+ return INT;
+ case "F":
+ return FLOAT;
+ case "J":
+ return LONG;
+ case "D":
+ return DOUBLE;
+ case "TOP":
+ return TOP;
+ case "NULL":
+ return NULL;
+ case "UNINITIALIZED_THIS":
+ return UNINITIALIZED_THIS;
+ default:
+ throw new RuntimeException("Invalid descriptor: " + descriptor);
+ }
+ }
+
+ abstract String descriptor();
+
+ @Override
+ public final String toString() {
+ return descriptor();
+ }
+
+ /** Is a category 2 value? */
+ boolean isCategory2() {
+ String descriptor = descriptor();
+ return descriptor.equals("J") || descriptor.equals("D");
+ }
+
+ /** If the type is an array, return the element type. Otherwise, throw an exception. */
+ InferredType getElementTypeIfArrayOrThrow() {
+ String descriptor = descriptor();
+ checkState(descriptor.charAt(0) == '[', "This type %s is not an array.", this);
+ return create(descriptor.substring(1));
+ }
+
+ /** Is an uninitialized value? */
+ boolean isUninitialized() {
+ return descriptor().startsWith(UNINITIALIZED_PREFIX);
+ }
+ }
+
+ private static final class InferenceClassVisitor extends ClassVisitor {
+ /**
+ * The method to visit.
+ *
+ * <p>We don't really <i>need</i> the method name: We could just visit the whole class, since we
+ * look at data for only the relevant line. But it's nice not to process the whole class,
+ * especially during debugging. (And it might also help avoid triggering any bugs in the
+ * inference code.)
+ */
+ private final String methodNameToVisit;
+
+ private final ImmutableSetMultimap.Builder<Integer, StackEntry> actualValueAtLine =
+ ImmutableSetMultimap.builder();
+ // TODO(cpovirk): Can the class visitor pass the name in?
+ private String className;
+
+ InferenceClassVisitor(String methodNameToVisit) {
+ super(Opcodes.ASM7);
+ this.methodNameToVisit = methodNameToVisit;
+ }
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ className = name;
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ /*
+ * Each InferenceMethodVisitor instance may be used only once. Still, it might seem like we
+ * can get away with creating a single instance at construction time. However, we know only
+ * the name of the method that we're visiting, not its full signature, so we may need to visit
+ * multiple methods with that name, each with a fresh visitor.
+ */
+ return methodNameToVisit.equals(name)
+ ? new InferenceMethodVisitor(access, className, name, desc, actualValueAtLine)
+ : null;
+ }
+ }
+
+ /*
+ * TODO(cpovirk): Expand this, maybe based on data about the most common method calls passed to
+ * assertThat().
+ */
+ private static final ImmutableSet<String> BORING_NAMES =
+ ImmutableSet.of(
+ "asList",
+ "build",
+ "collect",
+ "copyOf",
+ "create",
+ "from",
+ "get",
+ "iterator",
+ "of",
+ "toArray",
+ "toString",
+ "valueOf");
+
+ private static boolean isThatOrAssertThat(String owner, String name) {
+ /*
+ * TODO(cpovirk): Handle CustomSubjectBuilder. That requires looking at the type hierarchy, as
+ * users always have an instance of a specific subtype. Also keep in mind that the that(...)
+ * method might accept more than 1 parameter, like `that(className, methodName)` and/or that it
+ * might have category-2 parameters.
+ *
+ * TODO(cpovirk): Handle custom assertThat methods. The challenges are similar.
+ */
+ return (owner.equals("com/google/common/truth/Truth") && name.equals("assertThat"))
+ || (owner.equals("com/google/common/truth/StandardSubjectBuilder") && name.equals("that"))
+ || (owner.equals("com/google/common/truth/SimpleSubjectBuilder") && name.equals("that"));
+ }
+
+ private static boolean isBoxing(String owner, String name, String desc) {
+ return name.equals("valueOf")
+ && PRIMITIVE_WRAPPERS.contains(owner)
+ /*
+ * Don't handle valueOf(String s[, int radix]). The valueOf support is really here for
+ * autoboxing, as in "assertThat(primitive)," not for
+ * "assertThat(Integer.valueOf(...))." Not that there's anything really *wrong* with
+ * handling manual boxing of primitives -- good thing, since we can't distinguish the two --
+ * but we're not interested in handling the valueOf methods that *parse*. That's mainly
+ * because there's a type conversion, so some assertions might succeed on a string and fail
+ * on the parsed number (or vice versa).
+ */
+ && !Type.getArgumentTypes(desc)[0].equals(Type.getType(String.class));
+ }
+
+ private static final ImmutableSet<String> PRIMITIVE_WRAPPERS =
+ ImmutableSet.of(
+ "java/lang/Boolean",
+ "java/lang/Byte",
+ "java/lang/Character",
+ "java/lang/Double",
+ "java/lang/Float",
+ "java/lang/Integer",
+ "java/lang/Long",
+ "java/lang/Short");
+
+ private static boolean isStatic(int access) {
+ return isSet(access, Opcodes.ACC_STATIC);
+ }
+
+ /**
+ * Returns {@code true} iff <b>all</b> bits in {@code bitmask} are set in {@code flags}. Trivially
+ * returns {@code true} if {@code bitmask} is 0.
+ */
+ private static boolean isSet(int flags, int bitmask) {
+ return (flags & bitmask) == bitmask;
+ }
+
+ private static void closeQuietly(InputStream stream) {
+ if (stream == null) {
+ return;
+ }
+ try {
+ stream.close();
+ } catch (IOException e) {
+ // TODO(cpovirk): Log a warning?
+ }
+ }
+
+ /*
+ * TODO(cpovirk): Switch to using Checker Framework @Nullable. The problem I see if I try to
+ * switch now is that the AutoValue-generated code is `@Nullable ActualValueInference.StackEntry`
+ * rather than `ActualValueInference.@Nullable StackEntry`. AutoValue normally gets this right
+ * (b/29530042), so I think the failure here is because we use `-source 7`. That might still be
+ * fine, except that j2objc compilation appears to then use `-source 8`.
+ */
+ @interface Nullable {}
+
+ private ActualValueInference() {}
+}
diff --git a/core/src/main/java/com/google/common/truth/AssertionErrorWithFacts.java b/core/src/main/java/com/google/common/truth/AssertionErrorWithFacts.java
new file mode 100644
index 00000000..db465057
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/AssertionErrorWithFacts.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.truth.Fact.makeMessage;
+
+import com.google.common.collect.ImmutableList;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * An {@link AssertionError} composed of structured {@link Fact} instances and other string
+ * messages.
+ */
+@SuppressWarnings("OverrideThrowableToString") // We intentionally hide the class name.
+final class AssertionErrorWithFacts extends AssertionError implements ErrorWithFacts {
+ private final ImmutableList<Fact> facts;
+
+ /** Separate cause field, in case initCause() fails. */
+ private final @Nullable Throwable cause;
+
+ AssertionErrorWithFacts(
+ ImmutableList<String> messages, ImmutableList<Fact> facts, @Nullable Throwable cause) {
+ super(makeMessage(messages, facts));
+ this.facts = checkNotNull(facts);
+
+ this.cause = cause;
+ try {
+ initCause(cause);
+ } catch (IllegalStateException alreadyInitializedBecauseOfHarmonyBug) {
+ // See Truth.SimpleAssertionError.
+ }
+ }
+
+ @Override
+ @SuppressWarnings("UnsynchronizedOverridesSynchronized")
+ public Throwable getCause() {
+ return cause;
+ }
+
+ @Override
+ public String toString() {
+ return getLocalizedMessage();
+ }
+
+ @Override
+ public ImmutableList<Fact> facts() {
+ return facts;
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/BigDecimalSubject.java b/core/src/main/java/com/google/common/truth/BigDecimalSubject.java
new file mode 100644
index 00000000..64cf8236
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/BigDecimalSubject.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2015 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.Fact.simpleFact;
+
+import java.math.BigDecimal;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Propositions for {@link BigDecimal} typed subjects.
+ *
+ * @author Kurt Alfred Kluever
+ */
+public final class BigDecimalSubject extends ComparableSubject<BigDecimal> {
+ private final BigDecimal actual;
+
+ BigDecimalSubject(FailureMetadata metadata, @Nullable BigDecimal actual) {
+ super(metadata, actual);
+ this.actual = actual;
+ }
+
+ /**
+ * Fails if the subject's value is not equal to the value of the given {@link BigDecimal}. (i.e.,
+ * fails if {@code actual.comparesTo(expected) != 0}).
+ *
+ * <p><b>Note:</b> The scale of the BigDecimal is ignored. If you want to compare the values and
+ * the scales, use {@link #isEqualTo(Object)}.
+ */
+ public void isEqualToIgnoringScale(BigDecimal expected) {
+ compareValues(expected);
+ }
+
+ /**
+ * Fails if the subject's value is not equal to the value of the {@link BigDecimal} created from
+ * the expected string (i.e., fails if {@code actual.comparesTo(new BigDecimal(expected)) != 0}).
+ *
+ * <p><b>Note:</b> The scale of the BigDecimal is ignored. If you want to compare the values and
+ * the scales, use {@link #isEqualTo(Object)}.
+ */
+ public void isEqualToIgnoringScale(String expected) {
+ compareValues(new BigDecimal(expected));
+ }
+
+ /**
+ * Fails if the subject's value is not equal to the value of the {@link BigDecimal} created from
+ * the expected {@code long} (i.e., fails if {@code actual.comparesTo(new BigDecimal(expected)) !=
+ * 0}).
+ *
+ * <p><b>Note:</b> The scale of the BigDecimal is ignored. If you want to compare the values and
+ * the scales, use {@link #isEqualTo(Object)}.
+ */
+ public void isEqualToIgnoringScale(long expected) {
+ compareValues(new BigDecimal(expected));
+ }
+
+ /**
+ * Fails if the subject's value and scale is not equal to the given {@link BigDecimal}.
+ *
+ * <p><b>Note:</b> If you only want to compare the values of the BigDecimals and not their scales,
+ * use {@link #isEqualToIgnoringScale(BigDecimal)} instead.
+ */
+ @Override // To express more specific javadoc
+ public void isEqualTo(@Nullable Object expected) {
+ super.isEqualTo(expected);
+ }
+
+ /**
+ * Fails if the subject is not equivalent to the given value according to {@link
+ * Comparable#compareTo}, (i.e., fails if {@code a.comparesTo(b) != 0}). This method behaves
+ * identically to (the more clearly named) {@link #isEqualToIgnoringScale(BigDecimal)}.
+ *
+ * <p><b>Note:</b> Do not use this method for checking object equality. Instead, use {@link
+ * #isEqualTo(Object)}.
+ */
+ @Override
+ public void isEquivalentAccordingToCompareTo(BigDecimal expected) {
+ compareValues(expected);
+ }
+
+ private void compareValues(BigDecimal expected) {
+ if (actual.compareTo(expected) != 0) {
+ failWithoutActual(fact("expected", expected), butWas(), simpleFact("(scale is ignored)"));
+ }
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/BooleanSubject.java b/core/src/main/java/com/google/common/truth/BooleanSubject.java
new file mode 100644
index 00000000..528cbc62
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/BooleanSubject.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Fact.simpleFact;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Propositions for boolean subjects.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+public final class BooleanSubject extends Subject {
+ private final Boolean actual;
+
+ BooleanSubject(FailureMetadata metadata, @Nullable Boolean actual) {
+ super(metadata, actual);
+ this.actual = actual;
+ }
+
+ /** Fails if the subject is false or {@code null}. */
+ public void isTrue() {
+ if (actual == null) {
+ isEqualTo(true); // fails
+ } else if (!actual) {
+ failWithoutActual(simpleFact("expected to be true"));
+ }
+ }
+
+ /** Fails if the subject is true or {@code null}. */
+ public void isFalse() {
+ if (actual == null) {
+ isEqualTo(false); // fails
+ } else if (actual) {
+ failWithoutActual(simpleFact("expected to be false"));
+ }
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/ClassSubject.java b/core/src/main/java/com/google/common/truth/ClassSubject.java
new file mode 100644
index 00000000..a2ca5c48
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/ClassSubject.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import com.google.common.annotations.GwtIncompatible;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Propositions for {@link Class} subjects.
+ *
+ * @author Kurt Alfred Kluever
+ */
+@GwtIncompatible("reflection")
+public final class ClassSubject extends Subject {
+ private final Class<?> actual;
+
+ ClassSubject(FailureMetadata metadata, @Nullable Class<?> o) {
+ super(metadata, o);
+ this.actual = o;
+ }
+
+ /**
+ * Fails if this class or interface is not the same as or a subclass or subinterface of, the given
+ * class or interface.
+ */
+ public void isAssignableTo(Class<?> clazz) {
+ if (!clazz.isAssignableFrom(actual)) {
+ failWithActual("expected to be assignable to", clazz.getName());
+ }
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/ComparableSubject.java b/core/src/main/java/com/google/common/truth/ComparableSubject.java
new file mode 100644
index 00000000..ed769734
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/ComparableSubject.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import com.google.common.collect.Range;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Propositions for {@link Comparable} typed subjects.
+ *
+ * @author Kurt Alfred Kluever
+ * @param <T> the type of the object being tested by this {@code ComparableSubject}
+ */
+public abstract class ComparableSubject<T extends Comparable> extends Subject {
+ /**
+ * Constructor for use by subclasses. If you want to create an instance of this class itself, call
+ * {@link Subject#check(String, Object...) check(...)}{@code .that(actual)}.
+ */
+ private final T actual;
+
+ protected ComparableSubject(FailureMetadata metadata, @Nullable T actual) {
+ super(metadata, actual);
+ this.actual = actual;
+ }
+
+ /** Checks that the subject is in {@code range}. */
+ public final void isIn(Range<T> range) {
+ if (!range.contains(actual)) {
+ failWithActual("expected to be in range", range);
+ }
+ }
+
+ /** Checks that the subject is <i>not</i> in {@code range}. */
+ public final void isNotIn(Range<T> range) {
+ if (range.contains(actual)) {
+ failWithActual("expected not to be in range", range);
+ }
+ }
+
+ /**
+ * Checks that the subject is equivalent to {@code other} according to {@link
+ * Comparable#compareTo}, (i.e., checks that {@code a.comparesTo(b) == 0}).
+ *
+ * <p><b>Note:</b> Do not use this method for checking object equality. Instead, use {@link
+ * #isEqualTo(Object)}.
+ */
+ public void isEquivalentAccordingToCompareTo(T expected) {
+ if (actual.compareTo(expected) != 0) {
+ failWithActual("expected value that sorts equal to", expected);
+ }
+ }
+
+ /**
+ * Checks that the subject is greater than {@code other}.
+ *
+ * <p>To check that the subject is greater than <i>or equal to</i> {@code other}, use {@link
+ * #isAtLeast}.
+ */
+ public final void isGreaterThan(T other) {
+ if (actual.compareTo(other) <= 0) {
+ failWithActual("expected to be greater than", other);
+ }
+ }
+
+ /**
+ * Checks that the subject is less than {@code other}.
+ *
+ * <p>To check that the subject is less than <i>or equal to</i> {@code other}, use {@link
+ * #isAtMost}.
+ */
+ public final void isLessThan(T other) {
+ if (actual.compareTo(other) >= 0) {
+ failWithActual("expected to be less than", other);
+ }
+ }
+
+ /**
+ * Checks that the subject is less than or equal to {@code other}.
+ *
+ * <p>To check that the subject is <i>strictly</i> less than {@code other}, use {@link
+ * #isLessThan}.
+ */
+ public final void isAtMost(T other) {
+ if (actual.compareTo(other) > 0) {
+ failWithActual("expected to be at most", other);
+ }
+ }
+
+ /**
+ * Checks that the subject is greater than or equal to {@code other}.
+ *
+ * <p>To check that the subject is <i>strictly</i> greater than {@code other}, use {@link
+ * #isGreaterThan}.
+ */
+ public final void isAtLeast(T other) {
+ if (actual.compareTo(other) < 0) {
+ failWithActual("expected to be at least", other);
+ }
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/ComparisonFailureWithFacts.java b/core/src/main/java/com/google/common/truth/ComparisonFailureWithFacts.java
new file mode 100644
index 00000000..dcb4477f
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/ComparisonFailureWithFacts.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.truth.Fact.makeMessage;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.truth.Platform.PlatformComparisonFailure;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * An {@link AssertionError} (usually a JUnit {@code ComparisonFailure}, but not under GWT) composed
+ * of structured {@link Fact} instances and other string messages.
+ */
+final class ComparisonFailureWithFacts extends PlatformComparisonFailure implements ErrorWithFacts {
+ private final ImmutableList<Fact> facts;
+
+ ComparisonFailureWithFacts(
+ ImmutableList<String> messages,
+ ImmutableList<Fact> facts,
+ String expected,
+ String actual,
+ @Nullable Throwable cause) {
+ super(makeMessage(messages, facts), checkNotNull(expected), checkNotNull(actual), cause);
+ this.facts = checkNotNull(facts);
+ }
+
+ @Override
+ public ImmutableList<Fact> facts() {
+ return facts;
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/ComparisonFailures.java b/core/src/main/java/com/google/common/truth/ComparisonFailures.java
new file mode 100644
index 00000000..1e50f49c
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/ComparisonFailures.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Strings.commonPrefix;
+import static com.google.common.base.Strings.commonSuffix;
+import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.SubjectUtils.concat;
+import static java.lang.Character.isHighSurrogate;
+import static java.lang.Character.isLowSurrogate;
+import static java.lang.Math.max;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Contains part of the code responsible for creating a JUnit {@code ComparisonFailure} (if
+ * available) or a plain {@code AssertionError} (if not).
+ *
+ * <p>This particular class is responsible for the fallback when a platform offers {@code
+ * ComparisonFailure} but it is not available in a particular test environment. In practice, that
+ * should mean open-source JRE users who choose to exclude our JUnit 4 dependency.
+ *
+ * <p>(This class also includes logic to format expected and actual values for easier reading.)
+ *
+ * <p>Another part of the fallback logic is {@code Platform.ComparisonFailureWithFacts}, which has a
+ * different implementation under GWT/j2cl, where {@code ComparisonFailure} is also unavailable but
+ * we can't just recover from that at runtime.
+ */
+final class ComparisonFailures {
+ static ImmutableList<Fact> makeComparisonFailureFacts(
+ ImmutableList<Fact> headFacts,
+ ImmutableList<Fact> tailFacts,
+ String expected,
+ String actual) {
+ return concat(headFacts, formatExpectedAndActual(expected, actual), tailFacts);
+ }
+
+ /**
+ * Returns one or more facts describing the difference between the given expected and actual
+ * values.
+ *
+ * <p>Currently, that means either 2 facts (one each for expected and actual) or 1 fact with a
+ * diff-like (but much simpler) view.
+ *
+ * <p>In the case of 2 facts, the facts contain either the full expected and actual values or, if
+ * the values have a long prefix or suffix in common, abbreviated values with "
" at the beginning
+ * or end.
+ */
+ @VisibleForTesting
+ static ImmutableList<Fact> formatExpectedAndActual(String expected, String actual) {
+ ImmutableList<Fact> result;
+
+ // TODO(cpovirk): Call attention to differences in trailing whitespace.
+ // TODO(cpovirk): And changes in the *kind* of whitespace characters in the middle of the line.
+
+ result = Platform.makeDiff(expected, actual);
+ if (result != null) {
+ return result;
+ }
+
+ result = removeCommonPrefixAndSuffix(expected, actual);
+ if (result != null) {
+ return result;
+ }
+
+ return ImmutableList.of(fact("expected", expected), fact("but was", actual));
+ }
+
+ private static @Nullable ImmutableList<Fact> removeCommonPrefixAndSuffix(
+ String expected, String actual) {
+ int originalExpectedLength = expected.length();
+
+ // TODO(cpovirk): Use something like BreakIterator where available.
+ /*
+ * TODO(cpovirk): If the abbreviated values contain newlines, maybe expand them to contain a
+ * newline on each end so that we don't start mid-line? That way, horizontally aligned text will
+ * remain horizontally aligned. But of course, for many multi-line strings, we won't enter this
+ * method at all because we'll generate diff-style output instead. So we might not need to worry
+ * too much about newlines here.
+ */
+ // TODO(cpovirk): Avoid splitting in the middle of "\r\n."
+ int prefix = commonPrefix(expected, actual).length();
+ prefix = max(0, prefix - CONTEXT);
+ while (prefix > 0 && validSurrogatePairAt(expected, prefix - 1)) {
+ prefix--;
+ }
+ // No need to hide the prefix unless it's long.
+ if (prefix > 3) {
+ expected = "
" + expected.substring(prefix);
+ actual = "
" + actual.substring(prefix);
+ }
+
+ int suffix = commonSuffix(expected, actual).length();
+ suffix = max(0, suffix - CONTEXT);
+ while (suffix > 0 && validSurrogatePairAt(expected, expected.length() - suffix - 1)) {
+ suffix--;
+ }
+ // No need to hide the suffix unless it's long.
+ if (suffix > 3) {
+ expected = expected.substring(0, expected.length() - suffix) + "
";
+ actual = actual.substring(0, actual.length() - suffix) + "
";
+ }
+
+ if (originalExpectedLength - expected.length() < WORTH_HIDING) {
+ return null;
+ }
+
+ return ImmutableList.of(fact("expected", expected), fact("but was", actual));
+ }
+
+ private static final int CONTEXT = 20;
+ private static final int WORTH_HIDING = 60;
+
+ // From c.g.c.base.Strings.
+ private static boolean validSurrogatePairAt(CharSequence string, int index) {
+ return index >= 0
+ && index <= (string.length() - 2)
+ && isHighSurrogate(string.charAt(index))
+ && isLowSurrogate(string.charAt(index + 1));
+ }
+
+ private ComparisonFailures() {}
+}
diff --git a/core/src/main/java/com/google/common/truth/Correspondence.java b/core/src/main/java/com/google/common/truth/Correspondence.java
new file mode 100644
index 00000000..b4a04b0e
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/Correspondence.java
@@ -0,0 +1,841 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Functions.identity;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.truth.DoubleSubject.checkTolerance;
+import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.Fact.simpleFact;
+import static com.google.common.truth.Platform.getStackTraceAsString;
+import static java.util.Arrays.asList;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import java.util.Arrays;
+import java.util.List;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Determines whether an instance of type {@code A} corresponds in some way to an instance of type
+ * {@code E} for the purposes of a test assertion. For example, the implementation returned by the
+ * {@link #tolerance(double)} factory method implements approximate equality between numeric values,
+ * with values being said to correspond if the difference between them does not exceed the given
+ * fixed tolerance. The instances of type {@code A} are typically actual values from a collection
+ * returned by the code under test; the instances of type {@code E} are typically expected values
+ * with which the actual values are compared by the test.
+ *
+ * <p>The correspondence is required to be consistent: for any given values {@code actual} and
+ * {@code expected}, multiple invocations of {@code compare(actual, expected)} must consistently
+ * return {@code true} or consistently return {@code false} (provided that neither value is
+ * modified). Although {@code A} and {@code E} will often be the same types, they are <i>not</i>
+ * required to be the same, and even if they are it is <i>not</i> required that the correspondence
+ * should have any of the other properties of an equivalence relation (reflexivity, symmetry, or
+ * transitivity).
+ *
+ * <p>Optionally, instances of this class can also provide functionality to format the difference
+ * between values which do not correspond. This results in failure messages including formatted
+ * diffs between expected and actual value, where possible.
+ *
+ * <p>The recommended approach for creating an instance of this class is to use one of the static
+ * factory methods. The most general of these is {@link #from}; the other methods are more
+ * convenient in specific cases. The optional diff-formatting functionality can be added using
+ * {@link #formattingDiffsUsing}. (Alternatively, you can subclass this class yourself, but that is
+ * generally not recommended.)
+ *
+ * <p>Instances of this are typically used via {@link IterableSubject#comparingElementsUsing},
+ * {@link MapSubject#comparingValuesUsing}, or {@link MultimapSubject#comparingValuesUsing}.
+ *
+ * @author Pete Gillin
+ */
+public abstract class Correspondence<A, E> {
+
+ /**
+ * Constructs a {@link Correspondence} that compares actual and expected elements using the given
+ * binary predicate.
+ *
+ * <p>The correspondence does not support formatting of diffs (see {@link #formatDiff}). You can
+ * add that behaviour by calling {@link Correspondence#formattingDiffsUsing}.
+ *
+ * <p>Note that, if the data you are asserting about contains nulls, your predicate may be invoked
+ * with null arguments. If this causes it to throw a {@link NullPointerException}, then your test
+ * will fail. (See {@link Correspondence#compare} for more detail on how exceptions are handled.)
+ * In particular, if your predicate is an instance method reference on the actual value (as in the
+ * {@code String::contains} example below), your test will fail if it sees null actual values.
+ *
+ * <p>Example using an instance method reference:
+ *
+ * <pre>{@code
+ * static final Correspondence<String, String> CONTAINS_SUBSTRING =
+ * Correspondence.from(String::contains, "contains");
+ * }</pre>
+ *
+ * <p>Example using a static method reference:
+ *
+ * <pre>{@code
+ * class MyRecordTestHelper {
+ * static final Correspondence<MyRecord, MyRecord> EQUIVALENCE =
+ * Correspondence.from(MyRecordTestHelper::recordsEquivalent, "is equivalent to");
+ * static boolean recordsEquivalent(@Nullable MyRecord actual, @Nullable MyRecord expected) {
+ * // code to check whether records should be considered equivalent for testing purposes
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>Example using a lambda:
+ *
+ * <pre>{@code
+ * static final Correspondence<Object, Class<?>> INSTANCE_OF =
+ * Correspondence.from((obj, clazz) -> clazz.isInstance(obj), "is an instance of");
+ * }</pre>
+ *
+ * @param predicate a {@link BinaryPredicate} taking an actual and expected value (in that order)
+ * and returning whether the actual value corresponds to the expected value in some way
+ * @param description should fill the gap in a failure message of the form {@code "not true that
+ * <some actual element> is an element that <description> <some expected element>"}, e.g.
+ * {@code "contains"}, {@code "is an instance of"}, or {@code "is equivalent to"}
+ */
+ public static <A, E> Correspondence<A, E> from(
+ BinaryPredicate<A, E> predicate, String description) {
+ return new FromBinaryPredicate<>(predicate, description);
+ }
+
+ /**
+ * A functional interface for a binary predicate, to be used to test whether a pair of objects of
+ * types {@code A} and {@code E} satisfy some condition.
+ *
+ * <p>This interface will normally be implemented using a lambda or a method reference, and the
+ * resulting object will normally be passed directly to {@link Correspondence#from}. As a result,
+ * you should almost never see {@code BinaryPredicate} used as the type of a field or variable, or
+ * a return type.
+ */
+ public interface BinaryPredicate<A, E> {
+
+ /**
+ * Returns whether or not the actual and expected values satisfy the condition defined by this
+ * predicate.
+ */
+ boolean apply(@Nullable A actual, @Nullable E expected);
+ }
+
+ private static final class FromBinaryPredicate<A, E> extends Correspondence<A, E> {
+ private final BinaryPredicate<A, E> predicate;
+ private final String description;
+
+ private FromBinaryPredicate(BinaryPredicate<A, E> correspondencePredicate, String description) {
+ this.predicate = checkNotNull(correspondencePredicate);
+ this.description = checkNotNull(description);
+ }
+
+ @Override
+ public boolean compare(@Nullable A actual, @Nullable E expected) {
+ return predicate.apply(actual, expected);
+ }
+
+ @Override
+ public String toString() {
+ return description;
+ }
+ }
+
+ /**
+ * Constructs a {@link Correspondence} that compares elements by transforming the actual elements
+ * using the given function and testing for equality with the expected elements. If the
+ * transformed actual element (i.e. the output of the given function) is null, it will correspond
+ * to a null expected element.
+ *
+ * <p>The correspondence does not support formatting of diffs (see {@link #formatDiff}). You can
+ * add that behaviour by calling {@link Correspondence#formattingDiffsUsing}.
+ *
+ * <p>Note that, if you the data you are asserting about contains null actual values, your
+ * function may be invoked with a null argument. If this causes it to throw a {@link
+ * NullPointerException}, then your test will fail. (See {@link Correspondence#compare} for more
+ * detail on how exceptions are handled.) In particular, this applies if your function is an
+ * instance method reference on the actual value (as in the example below). If you want a null
+ * actual element to correspond to a null expected element, you must ensure that your function
+ * transforms a null input to a null output.
+ *
+ * <p>Example:
+ *
+ * <pre>{@code
+ * static final Correspondence<MyRecord, Integer> HAS_ID =
+ * Correspondence.transforming(MyRecord::getId, "has an ID of");
+ * }</pre>
+ *
+ * This can be used as follows:
+ *
+ * <pre>{@code
+ * assertThat(myRecords).comparingElementsUsing(HAS_ID).containsExactly(123, 456, 789);
+ * }</pre>
+ *
+ * @param actualTransform a {@link Function} taking an actual value and returning a new value
+ * which will be compared with an expected value to determine whether they correspond
+ * @param description should fill the gap in a failure message of the form {@code "not true that
+ * <some actual element> is an element that <description> <some expected element>"}, e.g.
+ * {@code "has an ID of"}
+ */
+ public static <A, E> Correspondence<A, E> transforming(
+ Function<A, ? extends E> actualTransform, String description) {
+ return new Transforming<>(actualTransform, identity(), description);
+ }
+
+ /**
+ * Constructs a {@link Correspondence} that compares elements by transforming the actual and the
+ * expected elements using the given functions and testing the transformed values for equality. If
+ * an actual element is transformed to null, it will correspond to an expected element that is
+ * also transformed to null.
+ *
+ * <p>The correspondence does not support formatting of diffs (see {@link #formatDiff}). You can
+ * add that behaviour by calling {@link Correspondence#formattingDiffsUsing}.
+ *
+ * <p>Note that, if you the data you are asserting about contains null actual or expected values,
+ * the appropriate function may be invoked with a null argument. If this causes it to throw a
+ * {@link NullPointerException}, then your test will fail. (See {@link Correspondence#compare} for
+ * more detail on how exceptions are handled.) In particular, this applies if your function is an
+ * instance method reference on the actual or expected value (as in the example below). If you
+ * want a null actual element to correspond to a null expected element, you must ensure that your
+ * functions both transform a null input to a null output.
+ *
+ * <p>If you want to apply the same function to both the actual and expected elements, just
+ * provide the same argument twice.
+ *
+ * <p>Example:
+ *
+ * <pre>{@code
+ * static final Correspondence<MyRequest, MyResponse> SAME_IDS =
+ * Correspondence.transforming(MyRequest::getId, MyResponse::getId, "has the same ID as");
+ * }</pre>
+ *
+ * This can be used as follows:
+ *
+ * <pre>{@code
+ * assertThat(myResponses).comparingElementsUsing(SAME_IDS).containsExactlyElementsIn(myRequests);
+ * }</pre>
+ *
+ * @param actualTransform a {@link Function} taking an actual value and returning a new value
+ * which will be compared with a transformed expected value to determine whether they
+ * correspond
+ * @param expectedTransform a {@link Function} taking an expected value and returning a new value
+ * which will be compared with a transformed actual value
+ * @param description should fill the gap in a failure message of the form {@code "not true that
+ * <some actual element> is an element that <description> <some expected element>"}, e.g.
+ * {@code "has the same ID as"}
+ */
+ public static <A, E> Correspondence<A, E> transforming(
+ Function<A, ?> actualTransform, Function<E, ?> expectedTransform, String description) {
+ return new Transforming<>(actualTransform, expectedTransform, description);
+ }
+
+ private static final class Transforming<A, E> extends Correspondence<A, E> {
+
+ private final Function<? super A, ?> actualTransform;
+ private final Function<? super E, ?> expectedTransform;
+ private final String description;
+
+ private Transforming(
+ Function<? super A, ?> actualTransform,
+ Function<? super E, ?> expectedTransform,
+ String description) {
+ this.actualTransform = actualTransform;
+ this.expectedTransform = expectedTransform;
+ this.description = description;
+ }
+
+ @Override
+ public boolean compare(@Nullable A actual, @Nullable E expected) {
+ return Objects.equal(actualTransform.apply(actual), expectedTransform.apply(expected));
+ }
+
+ @Override
+ public String toString() {
+ return description;
+ }
+ }
+
+ /**
+ * Returns a {@link Correspondence} between {@link Number} instances that considers instances to
+ * correspond (i.e. {@link Correspondence#compare(Object, Object)} returns {@code true}) if the
+ * double values of each instance (i.e. the result of calling {@link Number#doubleValue()} on
+ * them) are finite values within {@code tolerance} of each other.
+ *
+ * <ul>
+ * <li>It does not consider instances to correspond if either value is infinite or NaN.
+ * <li>The conversion to double may result in a loss of precision for some numeric types.
+ * <li>The {@link Correspondence#compare(Object, Object)} method throws a {@link
+ * NullPointerException} if either {@link Number} instance is null.
+ * </ul>
+ *
+ * @param tolerance an inclusive upper bound on the difference between the double values of the
+ * two {@link Number} instances, which must be a non-negative finite value, i.e. not {@link
+ * Double#NaN}, {@link Double#POSITIVE_INFINITY}, or negative, including {@code -0.0}
+ */
+ public static Correspondence<Number, Number> tolerance(double tolerance) {
+ return new TolerantNumericEquality(tolerance);
+ }
+
+ private static final class TolerantNumericEquality extends Correspondence<Number, Number> {
+
+ private final double tolerance;
+
+ private TolerantNumericEquality(double tolerance) {
+ checkTolerance(tolerance);
+ this.tolerance = tolerance;
+ }
+
+ @Override
+ public boolean compare(Number actual, Number expected) {
+ double actualDouble = checkNotNull(actual).doubleValue();
+ double expectedDouble = checkNotNull(expected).doubleValue();
+ return MathUtil.equalWithinTolerance(actualDouble, expectedDouble, tolerance);
+ }
+
+ @Override
+ public String toString() {
+ return "is a finite number within " + tolerance + " of";
+ }
+ }
+
+ /**
+ * Returns a correspondence which compares elements using object equality, i.e. giving the same
+ * assertions as you would get without a correspondence. This exists so that we can add a
+ * diff-formatting functionality to it. See e.g. {@link IterableSubject#formattingDiffsUsing}.
+ */
+ @SuppressWarnings("unchecked") // safe covariant cast
+ static <T> Correspondence<T, T> equality() {
+ return (Equality<T>) Equality.INSTANCE;
+ }
+
+ private static final class Equality<T> extends Correspondence<T, T> {
+
+ private static final Equality<Object> INSTANCE = new Equality<>();
+
+ @Override
+ public boolean compare(T actual, T expected) {
+ return Objects.equal(actual, expected);
+ }
+
+ @Override
+ public String toString() {
+ // This should normally not be used, since isEquality() returns true, but it should do
+ // something sensible anyway:
+ return "is equal to";
+ }
+
+ @Override
+ boolean isEquality() {
+ return true;
+ }
+ }
+
+ /**
+ * Constructor. Creating subclasses (anonymous or otherwise) of this class is <i>not
+ * recommended</i>, but is possible via this constructor. The recommended approach is to use the
+ * factory methods instead (see {@linkplain Correspondence class-level documentation}).
+ *
+ * @deprecated Construct an instance with the static factory methods instead. The most mechanical
+ * migration is usually to {@link #from}.
+ */
+ @Deprecated
+ Correspondence() {}
+
+ /**
+ * Returns a new correspondence which is like this one, except that the given formatter may be
+ * used to format the difference between a pair of elements that do not correspond.
+ *
+ * <p>Note that, if you the data you are asserting about contains null actual or expected values,
+ * the formatter may be invoked with a null argument. If this causes it to throw a {@link
+ * NullPointerException}, that will be taken to indicate that the values cannot be diffed. (See
+ * {@link Correspondence#formatDiff} for more detail on how exceptions are handled.) If you think
+ * null values are likely, it is slightly cleaner to have the formatter return null in that case
+ * instead of throwing.
+ *
+ * <p>Example:
+ *
+ * <pre>{@code
+ * class MyRecordTestHelper {
+ * static final Correspondence<MyRecord, MyRecord> EQUIVALENCE =
+ * Correspondence.from(MyRecordTestHelper::recordsEquivalent, "is equivalent to")
+ * .formattingDiffsUsing(MyRecordTestHelper::formatRecordDiff);
+ * static boolean recordsEquivalent(@Nullable MyRecord actual, @Nullable MyRecord expected) {
+ * // code to check whether records should be considered equivalent for testing purposes
+ * }
+ * static String formatRecordDiff(@Nullable MyRecord actual, @Nullable MyRecord expected) {
+ * // code to format the diff between the records
+ * }
+ * }
+ * }</pre>
+ */
+ public Correspondence<A, E> formattingDiffsUsing(DiffFormatter<? super A, ? super E> formatter) {
+ return new FormattingDiffs<>(this, formatter);
+ }
+
+ /**
+ * A functional interface to be used format the diff between a pair of objects of types {@code A}
+ * and {@code E}.
+ *
+ * <p>This interface will normally be implemented using a lambda or a method reference, and the
+ * resulting object will normally be passed directly to {@link
+ * Correspondence#formattingDiffsUsing}. As a result, you should almost never see {@code
+ * DiffFormatter} used as the type of a field or variable, or a return type.
+ */
+ public interface DiffFormatter<A, E> {
+
+ /**
+ * Returns a {@link String} describing the difference between the {@code actual} and {@code
+ * expected} values, if possible, or {@code null} if not.
+ */
+ @Nullable
+ String formatDiff(@Nullable A actual, @Nullable E expected);
+ }
+
+ private static class FormattingDiffs<A, E> extends Correspondence<A, E> {
+
+ private final Correspondence<A, E> delegate;
+ private final DiffFormatter<? super A, ? super E> formatter;
+
+ FormattingDiffs(Correspondence<A, E> delegate, DiffFormatter<? super A, ? super E> formatter) {
+ this.delegate = checkNotNull(delegate);
+ this.formatter = checkNotNull(formatter);
+ }
+
+ @Override
+ public boolean compare(@Nullable A actual, @Nullable E expected) {
+ return delegate.compare(actual, expected);
+ }
+
+ @Override
+ public @Nullable String formatDiff(@Nullable A actual, @Nullable E expected) {
+ return formatter.formatDiff(actual, expected);
+ }
+
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+
+ @Override
+ boolean isEquality() {
+ return delegate.isEquality();
+ }
+ }
+
+ /**
+ * Returns whether or not the {@code actual} value is said to correspond to the {@code expected}
+ * value for the purposes of this test.
+ *
+ * <h3>Exception handling</h3>
+ *
+ * <p>Throwing a {@link RuntimeException} from this method indicates that this {@link
+ * Correspondence} cannot compare the given values. Any assertion which encounters such an
+ * exception during the course of evaluating its condition must not pass. However, an assertion is
+ * not required to invoke this method for every pair of values in its input just in order to check
+ * for exceptions, if it is able to evaluate its condition without doing so.
+ *
+ * <h4>Conventions for handling exceptions</h4>
+ *
+ * <p>(N.B. This section is only really of interest when implementing assertion methods that call
+ * {@link Correspondence#compare}, not to users making such assertions in their tests.)
+ *
+ * <p>The only requirement on an assertion is that, if it encounters an exception from this
+ * method, it must not pass. The simplest implementation choice is simply to allow the exception
+ * to propagate. However, it is normally more helpful to catch the exception and instead fail with
+ * a message which includes more information about the assertion in progress and the nature of the
+ * failure.
+ *
+ * <p>By convention, an assertion may catch and store the exception and continue evaluating the
+ * condition as if the method had returned false instead of throwing. If the assertion's condition
+ * does not hold with this alternative behaviour, it may choose to fail with a message that gives
+ * details about how the condition does not hold, additionally mentioning that assertions were
+ * encountered and giving details about one of the stored exceptions. (See the first example
+ * below.) If the assertion's condition does hold with this alternative behaviour, the requirement
+ * that the assertion must not pass still applies, so it should fail with a message giving details
+ * about one of the stored exceptions. (See the second and third examples below.)
+ *
+ * <p>This behaviour is only a convention and should only be implemented when it makes sense to do
+ * so. In particular, in an assertion that has multiple stages, it may be better to only continue
+ * evaluation to the end of the current stage, and fail citing a stored exception at the end of
+ * the stage, rather than accumulating exceptions through the multiple stages.
+ *
+ * <h4>Examples of exception handling</h4>
+ *
+ * <p>Suppose that we have the correspondence
+ *
+ * <pre>{@code
+ * static final Correspondence<String, String> CASE_INSENSITIVE_EQUALITY =
+ * Correspondence.from(String::equalsIgnoreCase, "equals ignoring case"}
+ * }</pre>
+ *
+ * whose {@code compare} method throws {@link NullPointerException} if the actual value is null.
+ * The assertion
+ *
+ * <pre>{@code
+ * assertThat(asList(null, "xyz", "abc", "def"))
+ * .comparingElementsUsing(CASE_INSENSITIVE_EQUALITY)
+ * .containsExactly("ABC", "DEF", "GHI", "JKL");
+ * }</pre>
+ *
+ * may fail saying that the actual iterable contains unexpected values {@code null} and {@code
+ * xyz} and is missing values corresponding to {@code GHI} and {@code JKL}, which is what it would
+ * do if the {@code compare} method returned false instead of throwing, and additionally mention
+ * the exception. (This is more helpful than allowing the {@link NullPointerException} to
+ * propagate to the caller, or than failing with only a description of the exception.)
+ *
+ * <p>However, the assertions
+ *
+ * <pre>{@code
+ * assertThat(asList(null, "xyz", "abc", "def"))
+ * .comparingElementsUsing(CASE_INSENSITIVE_EQUALITY)
+ * .doesNotContain("MNO");
+ * }</pre>
+ *
+ * and
+ *
+ * <pre>{@code
+ * assertThat(asList(null, "xyz", "abc", "def"))
+ * .comparingElementsUsing(CASE_INSENSITIVE_EQUALITY)
+ * .doesNotContain(null);
+ * }</pre>
+ *
+ * must both fail citing the exception, even though they would pass if the {@code compare} method
+ * returned false. (Note that, in the latter case at least, it is likely that the test author's
+ * intention was <i>not</i> for the test to pass with these values.)
+ */
+ public abstract boolean compare(@Nullable A actual, @Nullable E expected);
+
+ private static class StoredException {
+
+ private static final Joiner ARGUMENT_JOINER = Joiner.on(", ").useForNull("null");
+
+ private final Exception exception;
+ private final String methodName;
+ private final List<Object> methodArguments;
+
+ StoredException(Exception exception, String methodName, List<Object> methodArguments) {
+ this.exception = checkNotNull(exception);
+ this.methodName = checkNotNull(methodName);
+ this.methodArguments = checkNotNull(methodArguments);
+ }
+
+ /**
+ * Returns a String describing the exception stored. This includes a stack trace (except under
+ * j2cl, where this is not available). It also has a separator at the end, so that when this
+ * appears at the end of an {@code AssertionError} message, the stack trace of the stored
+ * exception is distinguishable from the stack trace of the {@code AssertionError}.
+ */
+ private String describe() {
+ return Strings.lenientFormat(
+ "%s(%s) threw %s\n---",
+ methodName, ARGUMENT_JOINER.join(methodArguments), getStackTraceAsString(exception));
+ }
+ }
+
+ /**
+ * Helper object to store exceptions encountered while executing a {@link Correspondence} method.
+ */
+ static final class ExceptionStore {
+
+ private final String argumentLabel;
+ private StoredException firstCompareException = null;
+ private StoredException firstPairingException = null;
+ private StoredException firstFormatDiffException = null;
+
+ static ExceptionStore forIterable() {
+ return new ExceptionStore("elements");
+ }
+
+ static ExceptionStore forMapValues() {
+ return new ExceptionStore("values");
+ }
+
+ private ExceptionStore(String argumentLabel) {
+ this.argumentLabel = argumentLabel;
+ }
+
+ /**
+ * Adds an exception that was thrown during a {@code compare} call.
+ *
+ * @param callingClass The class from which the {@code compare} method was called. When
+ * reporting failures, stack traces will be truncated above elements in this class.
+ * @param exception The exception encountered
+ * @param actual The {@code actual} argument to the {@code compare} call during which the
+ * exception was encountered
+ * @param expected The {@code expected} argument to the {@code compare} call during which the
+ * exception was encountered
+ */
+ void addCompareException(
+ Class<?> callingClass, Exception exception, Object actual, Object expected) {
+ if (firstCompareException == null) {
+ truncateStackTrace(exception, callingClass);
+ firstCompareException = new StoredException(exception, "compare", asList(actual, expected));
+ }
+ }
+
+ /**
+ * Adds an exception that was thrown during an {@code apply} call on the function used to key
+ * actual elements.
+ *
+ * @param callingClass The class from which the {@code apply} method was called. When reporting
+ * failures, stack traces will be truncated above elements in this class.
+ * @param exception The exception encountered
+ * @param actual The {@code actual} argument to the {@code apply} call during which the
+ * exception was encountered
+ */
+ void addActualKeyFunctionException(Class<?> callingClass, Exception exception, Object actual) {
+ if (firstPairingException == null) {
+ truncateStackTrace(exception, callingClass);
+ firstPairingException =
+ new StoredException(exception, "actualKeyFunction.apply", asList(actual));
+ }
+ }
+
+ /**
+ * Adds an exception that was thrown during an {@code apply} call on the function used to key
+ * expected elements.
+ *
+ * @param callingClass The class from which the {@code apply} method was called. When reporting
+ * failures, stack traces will be truncated above elements in this class.
+ * @param exception The exception encountered
+ * @param expected The {@code expected} argument to the {@code apply} call during which the
+ * exception was encountered
+ */
+ void addExpectedKeyFunctionException(
+ Class<?> callingClass, Exception exception, Object expected) {
+ if (firstPairingException == null) {
+ truncateStackTrace(exception, callingClass);
+ firstPairingException =
+ new StoredException(exception, "expectedKeyFunction.apply", asList(expected));
+ }
+ }
+
+ /**
+ * Adds an exception that was thrown during a {@code formatDiff} call.
+ *
+ * @param callingClass The class from which the {@code formatDiff} method was called. When
+ * reporting failures, stack traces will be truncated above elements in this class.
+ * @param exception The exception encountered
+ * @param actual The {@code actual} argument to the {@code formatDiff} call during which the
+ * exception was encountered
+ * @param expected The {@code expected} argument to the {@code formatDiff} call during which the
+ * exception was encountered
+ */
+ void addFormatDiffException(
+ Class<?> callingClass, Exception exception, Object actual, Object expected) {
+ if (firstFormatDiffException == null) {
+ truncateStackTrace(exception, callingClass);
+ firstFormatDiffException =
+ new StoredException(exception, "formatDiff", asList(actual, expected));
+ }
+ }
+
+ /** Returns whether any exceptions thrown during {@code compare} calls were stored. */
+ boolean hasCompareException() {
+ return firstCompareException != null;
+ }
+
+ /**
+ * Returns facts to use in a failure message when the exceptions from {@code compare} calls are
+ * the main cause of the failure. At least one exception thrown during a {@code compare} call
+ * must have been stored, and no exceptions from a {@code formatDiff} call. Assertions should
+ * use this when exceptions were thrown while comparing elements and no more meaningful failure
+ * was discovered by assuming a false return and continuing (see the javadoc for {@link
+ * Correspondence#compare}). C.f. {@link #describeAsAdditionalInfo}.
+ */
+ ImmutableList<Fact> describeAsMainCause() {
+ checkState(firstCompareException != null);
+ // We won't do pairing or diff formatting unless a more meaningful failure was found, and if a
+ // more meaningful failure was found then we shouldn't be using this method:
+ checkState(firstPairingException == null);
+ checkState(firstFormatDiffException == null);
+ return ImmutableList.of(
+ simpleFact("one or more exceptions were thrown while comparing " + argumentLabel),
+ fact("first exception", firstCompareException.describe()));
+ }
+
+ /**
+ * If any exceptions are stored, returns facts to use in a failure message when the exceptions
+ * should be noted as additional info; if empty, returns an empty list. Assertions should use
+ * this when exceptions were thrown while comparing elements but more meaningful failures were
+ * discovered by assuming a false return and continuing (see the javadoc for {@link
+ * Correspondence#compare}), or when exceptions were thrown by other methods while generating
+ * the failure message. C.f. {@link #describeAsMainCause}.
+ */
+ ImmutableList<Fact> describeAsAdditionalInfo() {
+ ImmutableList.Builder<Fact> builder = ImmutableList.builder();
+ if (firstCompareException != null) {
+ builder.add(
+ simpleFact(
+ "additionally, one or more exceptions were thrown while comparing "
+ + argumentLabel));
+ builder.add(fact("first exception", firstCompareException.describe()));
+ }
+ if (firstPairingException != null) {
+ builder.add(
+ simpleFact(
+ "additionally, one or more exceptions were thrown while keying "
+ + argumentLabel
+ + " for pairing"));
+ builder.add(fact("first exception", firstPairingException.describe()));
+ }
+ if (firstFormatDiffException != null) {
+ builder.add(
+ simpleFact("additionally, one or more exceptions were thrown while formatting diffs"));
+ builder.add(fact("first exception", firstFormatDiffException.describe()));
+ }
+ return builder.build();
+ }
+
+ private static void truncateStackTrace(Exception exception, Class<?> callingClass) {
+ StackTraceElement[] original = exception.getStackTrace();
+ int keep = 0;
+ while (keep < original.length
+ && !original[keep].getClassName().equals(callingClass.getName())) {
+ keep++;
+ }
+ exception.setStackTrace(Arrays.copyOf(original, keep));
+ }
+ }
+
+ /**
+ * Invokes {@link #compare}, catching any exceptions. If the comparison does not throw, returns
+ * the result. If it does throw, adds the exception to the given {@link ExceptionStore} and
+ * returns false. This method can help with implementing the exception-handling policy described
+ * above, but note that assertions using it <i>must</i> fail later if an exception was stored.
+ */
+ final boolean safeCompare(@Nullable A actual, @Nullable E expected, ExceptionStore exceptions) {
+ try {
+ return compare(actual, expected);
+ } catch (RuntimeException e) {
+ exceptions.addCompareException(Correspondence.class, e, actual, expected);
+ return false;
+ }
+ }
+
+ /**
+ * Returns a {@link String} describing the difference between the {@code actual} and {@code
+ * expected} values, if possible, or {@code null} if not.
+ *
+ * <p>The implementation on the {@link Correspondence} base class always returns {@code null}. To
+ * enable diffing, use {@link #formattingDiffsUsing} (or override this method in a subclass, but
+ * factory methods are recommended over subclassing).
+ *
+ * <p>Assertions should only invoke this with parameters for which {@link #compare} returns {@code
+ * false}.
+ *
+ * <p>If this throws an exception, that implies that it is not possible to describe the diffs. An
+ * assertion will normally only call this method if it has established that its condition does not
+ * hold: good practice dictates that, if this method throws, the assertion should catch the
+ * exception and continue to describe the original failure as if this method had returned null,
+ * mentioning the failure from this method as additional information.
+ */
+ public @Nullable String formatDiff(@Nullable A actual, @Nullable E expected) {
+ return null;
+ }
+
+ /**
+ * Invokes {@link #formatDiff}, catching any exceptions. If the comparison does not throw, returns
+ * the result. If it does throw, adds the exception to the given {@link ExceptionStore} and
+ * returns null.
+ */
+ final @Nullable String safeFormatDiff(
+ @Nullable A actual, @Nullable E expected, ExceptionStore exceptions) {
+ try {
+ return formatDiff(actual, expected);
+ } catch (RuntimeException e) {
+ exceptions.addFormatDiffException(Correspondence.class, e, actual, expected);
+ return null;
+ }
+ }
+
+ /**
+ * Returns a description of the correspondence, suitable to fill the gap in a failure message of
+ * the form {@code "<some actual element> is an element that ... <some expected element>"}. Note
+ * that this is a fragment of a verb phrase which takes a singular subject.
+ *
+ * <p>Example 1: For a {@code Correspondence<String, Integer>} that tests whether the actual
+ * string parses to the expected integer, this would return {@code "parses to"} to result in a
+ * failure message of the form {@code "<some actual string> is an element that parses to <some
+ * expected integer>"}.
+ *
+ * <p>Example 2: For the {@code Correspondence<Number, Number>} returns by {@link #tolerance} this
+ * returns {@code "is a finite number within " + tolerance + " of"} to result in a failure message
+ * of the form {@code "<some actual number> is an element that is a finite number within 0.0001 of
+ * <some expected number>"}.
+ */
+ @Override
+ public abstract String toString();
+
+ /**
+ * Returns whether this is an equality correspondence, i.e. one returned by {@link #equality} or
+ * one whose {@link #compare} delegates to one returned by {@link #equality}.
+ */
+ boolean isEquality() {
+ return false;
+ }
+
+ /**
+ * Returns a list of {@link Fact} instance describing how this correspondence compares elements of
+ * an iterable. There will be one "testing whether" fact, unless this {@link #isEquality is an
+ * equality correspondence}, in which case the list will be empty.
+ */
+ final ImmutableList<Fact> describeForIterable() {
+ if (!isEquality()) {
+ return ImmutableList.of(
+ fact("testing whether", "actual element " + this + " expected element"));
+ } else {
+ return ImmutableList.of();
+ }
+ }
+
+ /**
+ * Returns a list of {@link Fact} instance describing how this correspondence compares values in a
+ * map (or multimap). There will be one "testing whether" fact, unless this {@link #isEquality is
+ * an equality correspondence}, in which case the list will be empty.
+ */
+ final ImmutableList<Fact> describeForMapValues() {
+ if (!isEquality()) {
+ return ImmutableList.of(fact("testing whether", "actual value " + this + " expected value"));
+ } else {
+ return ImmutableList.of();
+ }
+ }
+
+ /**
+ * @throws UnsupportedOperationException always
+ * @deprecated {@link Object#equals(Object)} is not supported. If you meant to compare objects
+ * using this {@link Correspondence}, use {@link #compare}.
+ */
+ @Deprecated
+ @Override
+ public final boolean equals(@Nullable Object o) {
+ throw new UnsupportedOperationException(
+ "Correspondence.equals(object) is not supported. If you meant to compare objects, use"
+ + " .compare(actual, expected) instead.");
+ }
+
+ /**
+ * @throws UnsupportedOperationException always
+ * @deprecated {@link Object#hashCode()} is not supported.
+ */
+ @Deprecated
+ @Override
+ public final int hashCode() {
+ throw new UnsupportedOperationException("Correspondence.hashCode() is not supported.");
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/CustomSubjectBuilder.java b/core/src/main/java/com/google/common/truth/CustomSubjectBuilder.java
new file mode 100644
index 00000000..3b4f6b11
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/CustomSubjectBuilder.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * In a fluent assertion chain, exposes one or more "custom" {@code that} methods, which accept a
+ * value under test and return a {@link Subject}.
+ *
+ * <p>(Note that the "custom" {@code that} methods are not defined on {@code CustomSubjectBuilder}
+ * itself, only on its subtypes, which are the types users actually interact with.)
+ *
+ * <p>For more information about the methods in this class, see <a
+ * href="https://truth.dev/faq#full-chain">this FAQ entry</a>.
+ *
+ * <h3>For people extending Truth</h3>
+ *
+ * <p>When you write a custom subject, see <a href="https://truth.dev/extension">our doc on
+ * extensions</a>. It explains the cases in which {@code CustomSubjectBuilder} is necessary, and it
+ * links to further instructions.
+ */
+public abstract class CustomSubjectBuilder {
+ /**
+ * In a fluent assertion chain, the argument to the "custom" overload of {@link
+ * StandardSubjectBuilder#about(CustomSubjectBuilder.Factory) about}, the method that specifies
+ * what kind of {@link Subject} to create.
+ *
+ * <p>For more information about the fluent chain, see <a
+ * href="https://truth.dev/faq#full-chain">this FAQ entry</a>.
+ *
+ * <h3>For people extending Truth</h3>
+ *
+ * <p>When you write a custom subject, see <a href="https://truth.dev/extension">our doc on
+ * extensions</a>. It explains the cases in which {@code CustomSubjectBuilder.Factory} is
+ * necessary.
+ */
+ public interface Factory<CustomSubjectBuilderT extends CustomSubjectBuilder> {
+ /** Creates a new {@link CustomSubjectBuilder} of the appropriate type. */
+ CustomSubjectBuilderT createSubjectBuilder(FailureMetadata metadata);
+ }
+
+ private final FailureMetadata metadata;
+
+ /** Constructor for use by subclasses. */
+ protected CustomSubjectBuilder(FailureMetadata metadata) {
+ this.metadata = checkNotNull(metadata);
+ }
+
+ /**
+ * Returns the {@link FailureMetadata} instance that {@code that} methods should pass to {@link
+ * Subject} constructors.
+ */
+ protected final FailureMetadata metadata() {
+ return metadata;
+ }
+
+ // TODO(user): Better enforce that subclasses implement a that() method.
+}
diff --git a/core/src/main/java/com/google/common/truth/DiffUtils.java b/core/src/main/java/com/google/common/truth/DiffUtils.java
new file mode 100644
index 00000000..43248a72
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/DiffUtils.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (c) 2020 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A custom implementation of the diff algorithm based on the solution described at
+ * https://en.wikipedia.org/wiki/Longest_common_subsequence_problem
+ *
+ * @author Yun Peng (pcloudy@google.com)
+ */
+final class DiffUtils {
+ // A list of unique strings appeared in compared texts.
+ // The index of each string is its incremental Id.
+ private final List<String> stringList = new ArrayList<>();
+ // A map to record each unique string and its incremental id.
+ private final Map<String, Integer> stringToId = new HashMap<>();
+ private int[] original;
+ private int[] revised;
+ // lcs[i][j] is the length of the longest common sequence of original[1..i] and revised[1..j].
+ private int[][] lcs;
+ private final List<Character> unifiedDiffType = new ArrayList<>();
+ private final List<Integer> unifiedDiffContentId = new ArrayList<>();
+ private final List<String> reducedUnifiedDiff = new ArrayList<>();
+ private int offsetHead = 0;
+ private int offsetTail = 0;
+
+ private List<String> diff(
+ List<String> originalLines, List<String> revisedLines, int contextSize) {
+ reduceEqualLinesFromHeadAndTail(originalLines, revisedLines, contextSize);
+ originalLines = originalLines.subList(offsetHead, originalLines.size() - offsetTail);
+ revisedLines = revisedLines.subList(offsetHead, revisedLines.size() - offsetTail);
+
+ original = new int[originalLines.size() + 1];
+ revised = new int[revisedLines.size() + 1];
+ lcs = new int[originalLines.size() + 1][revisedLines.size() + 1];
+
+ for (int i = 0; i < originalLines.size(); i++) {
+ original[i + 1] = getIdByLine(originalLines.get(i));
+ }
+ for (int i = 0; i < revisedLines.size(); i++) {
+ revised[i + 1] = getIdByLine(revisedLines.get(i));
+ }
+
+ for (int i = 1; i < original.length; i++) {
+ for (int j = 1; j < revised.length; j++) {
+ if (original[i] == revised[j]) {
+ lcs[i][j] = lcs[i - 1][j - 1] + 1;
+ } else {
+ lcs[i][j] = max(lcs[i][j - 1], lcs[i - 1][j]);
+ }
+ }
+ }
+
+ calcUnifiedDiff(originalLines.size(), revisedLines.size());
+
+ calcReducedUnifiedDiff(contextSize);
+
+ return reducedUnifiedDiff;
+ }
+
+ /** Calculate an incremental Id for a given string. */
+ private Integer getIdByLine(String line) {
+ int newId = stringList.size();
+ Integer existingId = stringToId.put(line, newId);
+ if (existingId == null) {
+ stringList.add(line);
+ return newId;
+ } else {
+ stringToId.put(line, existingId);
+ return existingId;
+ }
+ }
+
+ /** An optimization to reduce the problem size by removing equal lines from head and tail. */
+ private void reduceEqualLinesFromHeadAndTail(
+ List<String> original, List<String> revised, int contextSize) {
+ int head = 0;
+ int maxHead = min(original.size(), revised.size());
+ while (head < maxHead && original.get(head).equals(revised.get(head))) {
+ head++;
+ }
+ head = max(head - contextSize, 0);
+ offsetHead = head;
+
+ int tail = 0;
+ int maxTail = min(original.size() - head - contextSize, revised.size() - head - contextSize);
+ while (tail < maxTail
+ && original
+ .get(original.size() - 1 - tail)
+ .equals(revised.get(revised.size() - 1 - tail))) {
+ tail++;
+ }
+ tail = max(tail - contextSize, 0);
+ offsetTail = tail;
+ }
+
+ private void calcUnifiedDiff(int i, int j) {
+ while (i > 0 || j > 0) {
+ if (i > 0
+ && j > 0
+ && original[i] == revised[j]
+ // Make sure the diff output is identical to the diff command line tool when there are
+ // multiple solutions.
+ && lcs[i - 1][j - 1] + 1 > lcs[i - 1][j]
+ && lcs[i - 1][j - 1] + 1 > lcs[i][j - 1]) {
+ unifiedDiffType.add(' ');
+ unifiedDiffContentId.add(original[i]);
+ i--;
+ j--;
+ } else if (j > 0 && (i == 0 || lcs[i][j - 1] >= lcs[i - 1][j])) {
+ unifiedDiffType.add('+');
+ unifiedDiffContentId.add(revised[j]);
+ j--;
+ } else if (i > 0 && (j == 0 || lcs[i][j - 1] < lcs[i - 1][j])) {
+ unifiedDiffType.add('-');
+ unifiedDiffContentId.add(original[i]);
+ i--;
+ }
+ }
+ Collections.reverse(unifiedDiffType);
+ Collections.reverse(unifiedDiffContentId);
+ }
+
+ /**
+ * Generate the unified diff with a given context size
+ *
+ * @param contextSize The context size we should leave at the beginning and end of each block.
+ */
+ private void calcReducedUnifiedDiff(int contextSize) {
+ // The index of the next line we're going to process in fullDiff.
+ int next = 0;
+ // The number of lines in original/revised file after the diff lines we've processed.
+ int lineNumOrigin = offsetHead;
+ int lineNumRevised = offsetHead;
+ while (next < unifiedDiffType.size()) {
+ // The start and end index of the current block in fullDiff
+ int start;
+ int end;
+ // The start line number of the block in original/revised file.
+ int startLineOrigin;
+ int startLineRevised;
+ // Find the next diff line that is not an equal line.
+ while (next < unifiedDiffType.size() && unifiedDiffType.get(next).equals(' ')) {
+ next++;
+ lineNumOrigin++;
+ lineNumRevised++;
+ }
+ if (next == unifiedDiffType.size()) {
+ break;
+ }
+ // Calculate the start line index of the current block in fullDiff
+ start = max(0, next - contextSize);
+
+ // Record the start line number in original and revised file of the current block
+ startLineOrigin = lineNumOrigin - (next - start - 1);
+ startLineRevised = lineNumRevised - (next - start - 1);
+
+ // The number of consecutive equal lines in fullDiff, we must find at least
+ // contextSize * 2 + 1 equal lines to identify the end of the block.
+ int equalLines = 0;
+ // Let `end` points to the last non-equal diff line
+ end = next;
+ while (next < unifiedDiffType.size() && equalLines < contextSize * 2 + 1) {
+ if (unifiedDiffType.get(next).equals(' ')) {
+ equalLines++;
+ lineNumOrigin++;
+ lineNumRevised++;
+ } else {
+ equalLines = 0;
+ // Record the latest non-equal diff line
+ end = next;
+ if (unifiedDiffType.get(next).equals('-')) {
+ lineNumOrigin++;
+ } else {
+ // line starts with "+"
+ lineNumRevised++;
+ }
+ }
+ next++;
+ }
+ // Calculate the end line index of the current block in fullDiff
+ end = min(end + contextSize + 1, unifiedDiffType.size());
+
+ // Calculate the size of the block content in original/revised file
+ int blockSizeOrigin = lineNumOrigin - startLineOrigin - (next - end - 1);
+ int blockSizeRevised = lineNumRevised - startLineRevised - (next - end - 1);
+
+ StringBuilder header = new StringBuilder();
+ header
+ .append("@@ -")
+ .append(startLineOrigin)
+ .append(",")
+ .append(blockSizeOrigin)
+ .append(" +")
+ .append(startLineRevised)
+ .append(",")
+ .append(blockSizeRevised)
+ .append(" @@");
+
+ reducedUnifiedDiff.add(header.toString());
+ for (int i = start; i < end; i++) {
+ reducedUnifiedDiff.add(
+ unifiedDiffType.get(i) + stringList.get(unifiedDiffContentId.get(i)));
+ }
+ }
+ }
+
+ static List<String> generateUnifiedDiff(
+ List<String> original, List<String> revised, int contextSize) {
+ return new DiffUtils().diff(original, revised, contextSize);
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/DoubleSubject.java b/core/src/main/java/com/google/common/truth/DoubleSubject.java
new file mode 100644
index 00000000..a198bff8
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/DoubleSubject.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.Fact.simpleFact;
+import static com.google.common.truth.MathUtil.equalWithinTolerance;
+import static com.google.common.truth.MathUtil.notEqualWithinTolerance;
+import static com.google.common.truth.Platform.doubleToString;
+import static java.lang.Double.NaN;
+import static java.lang.Double.doubleToLongBits;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Propositions for {@link Double} subjects.
+ *
+ * @author Kurt Alfred Kluever
+ */
+public final class DoubleSubject extends ComparableSubject<Double> {
+ private static final long NEG_ZERO_BITS = doubleToLongBits(-0.0);
+
+ private final Double actual;
+
+ DoubleSubject(FailureMetadata metadata, @Nullable Double actual) {
+ super(metadata, actual);
+ this.actual = actual;
+ }
+
+ /**
+ * A partially specified check about an approximate relationship to a {@code double} subject using
+ * a tolerance.
+ */
+ public abstract static class TolerantDoubleComparison {
+
+ // Prevent subclassing outside of this class
+ private TolerantDoubleComparison() {}
+
+ /**
+ * Fails if the subject was expected to be within the tolerance of the given value but was not
+ * <i>or</i> if it was expected <i>not</i> to be within the tolerance but was. The subject and
+ * tolerance are specified earlier in the fluent call chain.
+ */
+ public abstract void of(double expectedDouble);
+
+ /**
+ * @throws UnsupportedOperationException always
+ * @deprecated {@link Object#equals(Object)} is not supported on TolerantDoubleComparison. If
+ * you meant to compare doubles, use {@link #of(double)} instead.
+ */
+ @Deprecated
+ @Override
+ public boolean equals(@Nullable Object o) {
+ throw new UnsupportedOperationException(
+ "If you meant to compare doubles, use .of(double) instead.");
+ }
+
+ /**
+ * @throws UnsupportedOperationException always
+ * @deprecated {@link Object#hashCode()} is not supported on TolerantDoubleComparison
+ */
+ @Deprecated
+ @Override
+ public int hashCode() {
+ throw new UnsupportedOperationException("Subject.hashCode() is not supported.");
+ }
+ }
+
+ /**
+ * Prepares for a check that the subject is a finite number within the given tolerance of an
+ * expected value that will be provided in the next call in the fluent chain.
+ *
+ * <p>The check will fail if either the subject or the object is {@link Double#POSITIVE_INFINITY},
+ * {@link Double#NEGATIVE_INFINITY}, or {@link Double#NaN}. To check for those values, use {@link
+ * #isPositiveInfinity}, {@link #isNegativeInfinity}, {@link #isNaN}, or (with more generality)
+ * {@link #isEqualTo}.
+ *
+ * <p>The check will pass if both values are zero, even if one is {@code 0.0} and the other is
+ * {@code -0.0}. Use {@code #isEqualTo} to assert that a value is exactly {@code 0.0} or that it
+ * is exactly {@code -0.0}.
+ *
+ * <p>You can use a tolerance of {@code 0.0} to assert the exact equality of finite doubles, but
+ * often {@link #isEqualTo} is preferable (note the different behaviours around non-finite values
+ * and {@code -0.0}). See the documentation on {@link #isEqualTo} for advice on when exact
+ * equality assertions are appropriate.
+ *
+ * @param tolerance an inclusive upper bound on the difference between the subject and object
+ * allowed by the check, which must be a non-negative finite value, i.e. not {@link
+ * Double#NaN}, {@link Double#POSITIVE_INFINITY}, or negative, including {@code -0.0}
+ */
+ public TolerantDoubleComparison isWithin(final double tolerance) {
+ return new TolerantDoubleComparison() {
+ @Override
+ public void of(double expected) {
+ Double actual = DoubleSubject.this.actual;
+ checkNotNull(
+ actual, "actual value cannot be null. tolerance=%s expected=%s", tolerance, expected);
+ checkTolerance(tolerance);
+
+ if (!equalWithinTolerance(actual, expected, tolerance)) {
+ failWithoutActual(
+ fact("expected", doubleToString(expected)),
+ butWas(),
+ fact("outside tolerance", doubleToString(tolerance)));
+ }
+ }
+ };
+ }
+
+ /**
+ * Prepares for a check that the subject is a finite number not within the given tolerance of an
+ * expected value that will be provided in the next call in the fluent chain.
+ *
+ * <p>The check will fail if either the subject or the object is {@link Double#POSITIVE_INFINITY},
+ * {@link Double#NEGATIVE_INFINITY}, or {@link Double#NaN}. See {@link #isFinite}, {@link
+ * #isNotNaN}, or {@link #isNotEqualTo} for checks with other behaviours.
+ *
+ * <p>The check will fail if both values are zero, even if one is {@code 0.0} and the other is
+ * {@code -0.0}. Use {@code #isNotEqualTo} for a test which fails for a value of exactly zero with
+ * one sign but passes for zero with the opposite sign.
+ *
+ * <p>You can use a tolerance of {@code 0.0} to assert the exact non-equality of finite doubles,
+ * but sometimes {@link #isNotEqualTo} is preferable (note the different behaviours around
+ * non-finite values and {@code -0.0}).
+ *
+ * @param tolerance an exclusive lower bound on the difference between the subject and object
+ * allowed by the check, which must be a non-negative finite value, i.e. not {@code
+ * Double.NaN}, {@code Double.POSITIVE_INFINITY}, or negative, including {@code -0.0}
+ */
+ public TolerantDoubleComparison isNotWithin(final double tolerance) {
+ return new TolerantDoubleComparison() {
+ @Override
+ public void of(double expected) {
+ Double actual = DoubleSubject.this.actual;
+ checkNotNull(
+ actual, "actual value cannot be null. tolerance=%s expected=%s", tolerance, expected);
+ checkTolerance(tolerance);
+
+ if (!notEqualWithinTolerance(actual, expected, tolerance)) {
+ failWithoutActual(
+ fact("expected not to be", doubleToString(expected)),
+ butWas(),
+ fact("within tolerance", doubleToString(tolerance)));
+ }
+ }
+ };
+ }
+
+ /**
+ * Asserts that the subject is exactly equal to the given value, with equality defined as by
+ * {@code Double#equals}. This method is <i>not</i> recommended when the code under test is doing
+ * any kind of arithmetic: use {@link #isWithin} with a suitable tolerance in that case. (Remember
+ * that the exact result of floating point arithmetic is sensitive to apparently trivial changes
+ * such as replacing {@code (a + b) + c} with {@code a + (b + c)}, and that unless {@code
+ * strictfp} is in force even the result of {@code (a + b) + c} is sensitive to the JVM's choice
+ * of precision for the intermediate result.) This method is recommended when the code under test
+ * is specified as either copying a value without modification from its input or returning a
+ * well-defined literal or constant value.
+ *
+ * <p><b>Note:</b> The assertion {@code isEqualTo(0.0)} fails for an input of {@code -0.0}, and
+ * vice versa. For an assertion that passes for either {@code 0.0} or {@code -0.0}, use {@link
+ * #isZero}.
+ */
+ @Override
+ public final void isEqualTo(@Nullable Object other) {
+ super.isEqualTo(other);
+ }
+
+ /**
+ * Asserts that the subject is not exactly equal to the given value, with equality defined as by
+ * {@code Double#equals}. See {@link #isEqualTo} for advice on when exact equality is recommended.
+ * Use {@link #isNotWithin} for an assertion with a tolerance.
+ *
+ * <p><b>Note:</b> The assertion {@code isNotEqualTo(0.0)} passes for {@code -0.0}, and vice
+ * versa. For an assertion that fails for either {@code 0.0} or {@code -0.0}, use {@link
+ * #isNonZero}.
+ */
+ @Override
+ public final void isNotEqualTo(@Nullable Object other) {
+ super.isNotEqualTo(other);
+ }
+
+ /**
+ * @deprecated Use {@link #isWithin} or {@link #isEqualTo} instead (see documentation for advice).
+ */
+ @Override
+ @Deprecated
+ public final void isEquivalentAccordingToCompareTo(Double other) {
+ super.isEquivalentAccordingToCompareTo(other);
+ }
+
+ /**
+ * Ensures that the given tolerance is a non-negative finite value, i.e. not {@code Double.NaN},
+ * {@code Double.POSITIVE_INFINITY}, or negative, including {@code -0.0}.
+ */
+ static void checkTolerance(double tolerance) {
+ checkArgument(!Double.isNaN(tolerance), "tolerance cannot be NaN");
+ checkArgument(tolerance >= 0.0, "tolerance (%s) cannot be negative", tolerance);
+ checkArgument(
+ doubleToLongBits(tolerance) != NEG_ZERO_BITS,
+ "tolerance (%s) cannot be negative",
+ tolerance);
+ checkArgument(tolerance != Double.POSITIVE_INFINITY, "tolerance cannot be POSITIVE_INFINITY");
+ }
+
+ /** Asserts that the subject is zero (i.e. it is either {@code 0.0} or {@code -0.0}). */
+ public final void isZero() {
+ if (actual == null || actual.doubleValue() != 0.0) {
+ failWithActual(simpleFact("expected zero"));
+ }
+ }
+
+ /**
+ * Asserts that the subject is a non-null value other than zero (i.e. it is not {@code 0.0},
+ * {@code -0.0} or {@code null}).
+ */
+ public final void isNonZero() {
+ if (actual == null) {
+ failWithActual(simpleFact("expected a double other than zero"));
+ } else if (actual.doubleValue() == 0.0) {
+ failWithActual(simpleFact("expected not to be zero"));
+ }
+ }
+
+ /** Asserts that the subject is {@link Double#POSITIVE_INFINITY}. */
+ public final void isPositiveInfinity() {
+ isEqualTo(Double.POSITIVE_INFINITY);
+ }
+
+ /** Asserts that the subject is {@link Double#NEGATIVE_INFINITY}. */
+ public final void isNegativeInfinity() {
+ isEqualTo(Double.NEGATIVE_INFINITY);
+ }
+
+ /** Asserts that the subject is {@link Double#NaN}. */
+ public final void isNaN() {
+ isEqualTo(NaN);
+ }
+
+ /**
+ * Asserts that the subject is finite, i.e. not {@link Double#POSITIVE_INFINITY}, {@link
+ * Double#NEGATIVE_INFINITY}, or {@link Double#NaN}.
+ */
+ public final void isFinite() {
+ if (actual == null || actual.isNaN() || actual.isInfinite()) {
+ failWithActual(simpleFact("expected to be finite"));
+ }
+ }
+
+ /**
+ * Asserts that the subject is a non-null value other than {@link Double#NaN} (but it may be
+ * {@link Double#POSITIVE_INFINITY} or {@link Double#NEGATIVE_INFINITY}).
+ */
+ public final void isNotNaN() {
+ if (actual == null) {
+ failWithActual(simpleFact("expected a double other than NaN"));
+ } else {
+ isNotEqualTo(NaN);
+ }
+ }
+
+ /**
+ * Checks that the subject is greater than {@code other}.
+ *
+ * <p>To check that the subject is greater than <i>or equal to</i> {@code other}, use {@link
+ * #isAtLeast}.
+ */
+ public final void isGreaterThan(int other) {
+ isGreaterThan((double) other);
+ }
+
+ /**
+ * Checks that the subject is less than {@code other}.
+ *
+ * <p>To check that the subject is less than <i>or equal to</i> {@code other}, use {@link
+ * #isAtMost} .
+ */
+ public final void isLessThan(int other) {
+ isLessThan((double) other);
+ }
+
+ /**
+ * Checks that the subject is less than or equal to {@code other}.
+ *
+ * <p>To check that the subject is <i>strictly</i> less than {@code other}, use {@link
+ * #isLessThan}.
+ */
+ public final void isAtMost(int other) {
+ isAtMost((double) other);
+ }
+
+ /**
+ * Checks that the subject is greater than or equal to {@code other}.
+ *
+ * <p>To check that the subject is <i>strictly</i> greater than {@code other}, use {@link
+ * #isGreaterThan}.
+ */
+ public final void isAtLeast(int other) {
+ isAtLeast((double) other);
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/ErrorWithFacts.java b/core/src/main/java/com/google/common/truth/ErrorWithFacts.java
new file mode 100644
index 00000000..61f404b6
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/ErrorWithFacts.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2018 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Supertype of Truth's {@link AssertionError} subclasses that are created from a list of {@link
+ * Fact} instances.
+ */
+interface ErrorWithFacts {
+ ImmutableList<Fact> facts();
+}
diff --git a/core/src/main/java/com/google/common/truth/Expect.java b/core/src/main/java/com/google/common/truth/Expect.java
new file mode 100644
index 00000000..5acc31d9
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/Expect.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Strings.padStart;
+import static com.google.common.base.Strings.repeat;
+import static com.google.common.base.Throwables.getStackTraceAsString;
+import static com.google.common.truth.Expect.TestPhase.AFTER;
+import static com.google.common.truth.Expect.TestPhase.BEFORE;
+import static com.google.common.truth.Expect.TestPhase.DURING;
+
+import com.google.common.annotations.GwtIncompatible;
+import com.google.common.base.Throwables;
+import com.google.common.truth.Truth.SimpleAssertionError;
+import com.google.errorprone.annotations.concurrent.GuardedBy;
+import java.util.ArrayList;
+import java.util.List;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.junit.internal.AssumptionViolatedException;
+import org.junit.rules.ErrorCollector;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * A {@link TestRule} that batches up all failures encountered during a test, and reports them all
+ * together at the end (similar to {@link ErrorCollector}). It is also useful for making assertions
+ * from other threads or from within callbacks whose exceptions would be swallowed or logged, rather
+ * than propagated out to fail the test. (<a href="https://joel-costigliola.github.io/assertj/"
+ * target="_top">AssertJ</a> has a similar feature called "soft assertions"; however, soft
+ * assertions are not safe for concurrent use.)
+ *
+ * <p>Usage:
+ *
+ * <pre>
+ * {@code @Rule public final Expect expect = Expect.create();}
+ *
+ * {@code ...}
+ *
+ * {@code expect.that(results).containsExactly(...);}
+ * {@code expect.that(errors).isEmpty();}
+ * </pre>
+ *
+ * If both of the assertions above fail, the test will fail with an exception that contains
+ * information about both.
+ *
+ * <p>{@code Expect} may be used concurrently from multiple threads. However, multithreaded tests
+ * still require care:
+ *
+ * <ul>
+ * <li>{@code Expect} has no way of knowing when all your other test threads are done. It simply
+ * checks for failures when the main thread finishes executing the test method. Thus, you must
+ * ensure that any background threads complete their assertions before then, or your test may
+ * ignore their results.
+ * <li>Assertion failures are not the only exceptions that may occur in other threads. For maximum
+ * safety, multithreaded tests should check for such exceptions regardless of whether they use
+ * {@code Expect}. (Typically, this means calling {@code get()} on any {@code Future} returned
+ * by a method like {@code executor.submit(...)}. It might also include checking for
+ * unexpected log messages
+ * or reading metrics that count failures.) If your tests already check for exceptions from a
+ * thread, then that will any cover exception from plain {@code assertThat}.
+ * </ul>
+ *
+ * <p>To record failures for the purpose of testing that an assertion fails when it should, see
+ * {@link ExpectFailure}.
+ *
+ * <p>For more on this class, see <a href="https://truth.dev/expect">the documentation page</a>.
+ */
+@GwtIncompatible("JUnit4")
+public final class Expect extends StandardSubjectBuilder implements TestRule {
+
+ private static final class ExpectationGatherer implements FailureStrategy {
+ @GuardedBy("this")
+ private final List<AssertionError> failures = new ArrayList<AssertionError>();
+
+ @GuardedBy("this")
+ private TestPhase inRuleContext = BEFORE;
+
+ ExpectationGatherer() {}
+
+ @Override
+ public synchronized void fail(AssertionError failure) {
+ record(failure);
+ }
+
+ synchronized void enterRuleContext() {
+ checkState(inRuleContext == BEFORE);
+ inRuleContext = DURING;
+ }
+
+ synchronized void leaveRuleContext(@Nullable Throwable caught) throws Throwable {
+ try {
+ if (caught == null) {
+ doLeaveRuleContext();
+ } else {
+ doLeaveRuleContext(caught);
+ }
+ /*
+ * We'd like to check this even if an exception was thrown, but we don't want to override
+ * the "real" failure. TODO(cpovirk): Maybe attach as a suppressed exception once we require
+ * a newer version of Android.
+ */
+ checkState(inRuleContext == DURING);
+ } finally {
+ inRuleContext = AFTER;
+ }
+ }
+
+ synchronized void checkInRuleContext() {
+ doCheckInRuleContext(null);
+ }
+
+ synchronized boolean hasFailures() {
+ return !failures.isEmpty();
+ }
+
+ @Override
+ public synchronized String toString() {
+ if (failures.isEmpty()) {
+ return "No expectation failed.";
+ }
+ int numFailures = failures.size();
+ StringBuilder message =
+ new StringBuilder(
+ numFailures + (numFailures > 1 ? " expectations" : " expectation") + " failed:\n");
+ int countLength = String.valueOf(failures.size() + 1).length();
+ int count = 0;
+ for (AssertionError failure : failures) {
+ count++;
+ message.append(" ");
+ message.append(padStart(String.valueOf(count), countLength, ' '));
+ message.append(". ");
+ if (count == 1) {
+ appendIndented(countLength, message, getStackTraceAsString(failure));
+ } else {
+ appendIndented(
+ countLength,
+ message,
+ printSubsequentFailure(failures.get(0).getStackTrace(), failure));
+ }
+ message.append("\n");
+ }
+
+ return message.toString();
+ }
+
+ private static void appendIndented(int countLength, StringBuilder builder, String toAppend) {
+ int indent = countLength + 4; // " " and ". "
+ builder.append(toAppend.replace("\n", "\n" + repeat(" ", indent)));
+ }
+
+ private String printSubsequentFailure(
+ StackTraceElement[] baseTraceFrames, AssertionError toPrint) {
+ Exception e = new RuntimeException("__EXCEPTION_MARKER__", toPrint);
+ e.setStackTrace(baseTraceFrames);
+ String s = Throwables.getStackTraceAsString(e);
+ // Force single line reluctant matching
+ return s.replaceFirst("(?s)^.*?__EXCEPTION_MARKER__.*?Caused by:\\s+", "");
+ }
+
+ @GuardedBy("this")
+ private void doCheckInRuleContext(@Nullable AssertionError failure) {
+ switch (inRuleContext) {
+ case BEFORE:
+ throw new IllegalStateException(
+ "assertion made on Expect instance, but it's not enabled as a @Rule.", failure);
+ case DURING:
+ return;
+ case AFTER:
+ throw new IllegalStateException(
+ "assertion made on Expect instance, but its @Rule has already completed. Maybe "
+ + "you're making assertions from a background thread and not waiting for them to "
+ + "complete, or maybe you've shared an Expect instance across multiple tests? "
+ + "We're throwing this exception to warn you that your assertion would have been "
+ + "ignored. However, this exception might not cause any test to fail, or it "
+ + "might cause some subsequent test to fail rather than the test that caused the "
+ + "problem.",
+ failure);
+ }
+ throw new AssertionError();
+ }
+
+ @GuardedBy("this")
+ private void doLeaveRuleContext() {
+ if (hasFailures()) {
+ throw SimpleAssertionError.createWithNoStack(this.toString());
+ }
+ }
+
+ @GuardedBy("this")
+ private void doLeaveRuleContext(Throwable caught) throws Throwable {
+ if (hasFailures()) {
+ String message =
+ caught instanceof AssumptionViolatedException
+ ? "Also, after those failures, an assumption was violated:"
+ : "Also, after those failures, an exception was thrown:";
+ record(SimpleAssertionError.createWithNoStack(message, caught));
+ throw SimpleAssertionError.createWithNoStack(this.toString());
+ } else {
+ throw caught;
+ }
+ }
+
+ @GuardedBy("this")
+ private void record(AssertionError failure) {
+ doCheckInRuleContext(failure);
+ failures.add(failure);
+ }
+ }
+
+ private final ExpectationGatherer gatherer;
+
+ /** Creates a new instance. */
+ public static Expect create() {
+ return new Expect(new ExpectationGatherer());
+ }
+
+ private Expect(ExpectationGatherer gatherer) {
+ super(FailureMetadata.forFailureStrategy(gatherer));
+ this.gatherer = checkNotNull(gatherer);
+ }
+
+ public boolean hasFailures() {
+ return gatherer.hasFailures();
+ }
+
+ @Override
+ void checkStatePreconditions() {
+ gatherer.checkInRuleContext();
+ }
+
+ @Override
+ public Statement apply(final Statement base, Description description) {
+ checkNotNull(base);
+ checkNotNull(description);
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ gatherer.enterRuleContext();
+ Throwable caught = null;
+ try {
+ base.evaluate();
+ } catch (Throwable t) {
+ caught = t;
+ } finally {
+ gatherer.leaveRuleContext(caught);
+ }
+ }
+ };
+ }
+
+ enum TestPhase {
+ BEFORE,
+ DURING,
+ AFTER;
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/ExpectFailure.java b/core/src/main/java/com/google/common/truth/ExpectFailure.java
new file mode 100644
index 00000000..970f5261
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/ExpectFailure.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (c) 2017 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Strings.lenientFormat;
+import static com.google.common.truth.Truth.assertAbout;
+import static com.google.common.truth.TruthFailureSubject.truthFailures;
+
+import com.google.common.annotations.GwtIncompatible;
+import com.google.common.truth.Truth.SimpleAssertionError;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * A utility for testing that assertions against a custom {@link Subject} fail when they should,
+ * plus a utility to assert about parts of the resulting failure messages.
+ *
+ * <p>Usage:
+ *
+ * <pre>{@code
+ * AssertionError failure =
+ * expectFailure(whenTesting -> whenTesting.that(cancelButton).isVisible());
+ * assertThat(failure).factKeys().containsExactly("expected to be visible");
+ *
+ * ...
+ *
+ * private static AssertionError expectFailure(
+ * ExpectFailure.SimpleSubjectBuilderCallback<UiElementSubject, UiElement> assertionCallback) {
+ * return ExpectFailure.expectFailureAbout(uiElements(), assertionCallback);
+ * }
+ * }</pre>
+ *
+ * Or, if you can't use lambdas:
+ *
+ * <pre>
+ * {@code @Rule public final ExpectFailure expectFailure = new ExpectFailure();}
+ *
+ * {@code ...
+ *
+ * expectFailure.whenTesting().about(uiElements()).that(cancelButton).isVisible();
+ * assertThat(failure).factKeys().containsExactly("expected to be visible");
+ * }</pre>
+ *
+ * <p>{@code ExpectFailure} is similar to JUnit's {@code assertThrows} (<a
+ * href="https://junit.org/junit4/javadoc/latest/org/junit/Assert.html#assertThrows%28java.lang.Class,%20org.junit.function.ThrowingRunnable%29">JUnit
+ * 4</a>, <a
+ * href="https://junit.org/junit5/docs/current/api/org/junit/jupiter/api/Assertions.html#assertThrows%28java.lang.Class,org.junit.jupiter.api.function.Executable%29">JUnit
+ * 5</a>). We recommend it over {@code assertThrows} when you're testing a Truth subject because it
+ * also checks that the assertion you're testing uses the supplied {@link FailureStrategy} and calls
+ * {@link FailureStrategy#fail} only once.
+ */
+public final class ExpectFailure implements Platform.JUnitTestRule {
+ private final FailureStrategy strategy =
+ new FailureStrategy() {
+ @Override
+ public void fail(AssertionError failure) {
+ captureFailure(failure);
+ }
+ };
+
+ private boolean inRuleContext = false;
+ private boolean failureExpected = false;
+ private @Nullable AssertionError failure = null;
+
+ /**
+ * Creates a new instance for use as a {@code @Rule}. See the class documentation for details, and
+ * consider using {@linkplain #expectFailure the lambda version} instead.
+ */
+ public ExpectFailure() {}
+
+ /**
+ * Returns a test verb that expects the chained assertion to fail, and makes the failure available
+ * via {@link #getFailure}.
+ *
+ * <p>An instance of {@code ExpectFailure} supports only one {@code whenTesting} call per test
+ * method. The static {@link #expectFailure} method, by contrast, does not have this limitation.
+ */
+ public StandardSubjectBuilder whenTesting() {
+ checkState(inRuleContext, "ExpectFailure must be used as a JUnit @Rule");
+ if (failure != null) {
+ throw SimpleAssertionError.create("ExpectFailure already captured a failure", failure);
+ }
+ if (failureExpected) {
+ throw new AssertionError(
+ "ExpectFailure.whenTesting() called previously, but did not capture a failure.");
+ }
+ failureExpected = true;
+ return StandardSubjectBuilder.forCustomFailureStrategy(strategy);
+ }
+
+ /**
+ * Enters rule context to be ready to capture failures.
+ *
+ * <p>This should be rarely used directly, except if this class is as a long living object but not
+ * as a JUnit rule, like truth subject tests where for GWT compatible reasons.
+ */
+ void enterRuleContext() {
+ this.inRuleContext = true;
+ }
+
+ /** Leaves rule context and verify if a failure has been caught if it's expected. */
+ void leaveRuleContext() {
+ this.inRuleContext = false;
+ }
+
+ /**
+ * Ensures a failure is caught if it's expected (i.e., {@link #whenTesting} is called) and throws
+ * error if not.
+ */
+ void ensureFailureCaught() {
+ if (failureExpected && failure == null) {
+ throw new AssertionError(
+ "ExpectFailure.whenTesting() invoked, but no failure was caught."
+ + Platform.EXPECT_FAILURE_WARNING_IF_GWT);
+ }
+ }
+
+ /** Returns the captured failure, if one occurred. */
+ public AssertionError getFailure() {
+ if (failure == null) {
+ throw new AssertionError("ExpectFailure did not capture a failure.");
+ }
+ return failure;
+ }
+
+ /**
+ * Captures the provided failure, or throws an {@link AssertionError} if a failure had previously
+ * been captured.
+ */
+ private void captureFailure(AssertionError captured) {
+ if (failure != null) {
+ // TODO(diamondm) is it worthwhile to add the failures as suppressed exceptions?
+ throw new AssertionError(
+ lenientFormat(
+ "ExpectFailure.whenTesting() caught multiple failures:\n\n%s\n\n%s\n",
+ Platform.getStackTraceAsString(failure), Platform.getStackTraceAsString(captured)));
+ }
+ failure = captured;
+ }
+
+ /**
+ * Static alternative that directly returns the triggered failure. This is intended to be used in
+ * Java 8+ tests similar to {@code expectThrows()}:
+ *
+ * <p>{@code AssertionError failure = expectFailure(whenTesting ->
+ * whenTesting.that(4).isNotEqualTo(4));}
+ */
+ public static AssertionError expectFailure(StandardSubjectBuilderCallback assertionCallback) {
+ ExpectFailure expectFailure = new ExpectFailure();
+ expectFailure.enterRuleContext(); // safe since this instance doesn't leave this method
+ assertionCallback.invokeAssertion(expectFailure.whenTesting());
+ return expectFailure.getFailure();
+ }
+
+ /**
+ * Static alternative that directly returns the triggered failure. This is intended to be used in
+ * Java 8+ tests similar to {@code expectThrows()}:
+ *
+ * <p>{@code AssertionError failure = expectFailureAbout(myTypes(), whenTesting ->
+ * whenTesting.that(myType).hasProperty());}
+ */
+ public static <S extends Subject, A> AssertionError expectFailureAbout(
+ final Subject.Factory<S, A> factory,
+ final SimpleSubjectBuilderCallback<S, A> assertionCallback) {
+ // whenTesting -> assertionCallback.invokeAssertion(whenTesting.about(factory))
+ return expectFailure(
+ new StandardSubjectBuilderCallback() {
+ @Override
+ public void invokeAssertion(StandardSubjectBuilder whenTesting) {
+ assertionCallback.invokeAssertion(whenTesting.about(factory));
+ }
+ });
+ }
+
+ /**
+ * Creates a subject for asserting about the given {@link AssertionError}, usually one produced by
+ * Truth.
+ */
+ public static TruthFailureSubject assertThat(AssertionError actual) {
+ return assertAbout(truthFailures()).that(actual);
+ }
+
+ @Override
+ @GwtIncompatible("org.junit.rules.TestRule")
+ public Statement apply(final Statement base, Description description) {
+ checkNotNull(base);
+ checkNotNull(description);
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ enterRuleContext();
+ try {
+ base.evaluate();
+ } finally {
+ leaveRuleContext();
+ }
+ ensureFailureCaught();
+ }
+ };
+ }
+
+ /**
+ * A "functional interface" for {@link #expectFailure expectFailure()} to invoke and capture
+ * failures.
+ *
+ * <p>Java 8+ users should pass a lambda to {@code .expectFailure()} rather than directly
+ * implement this interface. Java 7+ users can define an {@code @Rule ExpectFailure} instance
+ * instead, however if you prefer the {@code .expectFailure()} pattern you can use this interface
+ * to pass in an anonymous class.
+ */
+ public interface StandardSubjectBuilderCallback {
+ void invokeAssertion(StandardSubjectBuilder whenTesting);
+ }
+
+ /**
+ * A "functional interface" for {@link #expectFailureAbout expectFailureAbout()} to invoke and
+ * capture failures.
+ *
+ * <p>Java 8+ users should pass a lambda to {@code .expectFailureAbout()} rather than directly
+ * implement this interface. Java 7+ users can define an {@code @Rule ExpectFailure} instance
+ * instead, however if you prefer the {@code .expectFailureAbout()} pattern you can use this
+ * interface to pass in an anonymous class.
+ */
+ public interface SimpleSubjectBuilderCallback<S extends Subject, A> {
+ void invokeAssertion(SimpleSubjectBuilder<S, A> whenTesting);
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/Fact.java b/core/src/main/java/com/google/common/truth/Fact.java
new file mode 100644
index 00000000..02e2bea4
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/Fact.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) 2018 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Strings.padEnd;
+import static java.lang.Math.max;
+
+import com.google.common.collect.ImmutableList;
+import java.io.Serializable;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A string key-value pair in a failure message, such as "expected: abc" or "but was: xyz."
+ *
+ * <p>Most Truth users will never interact with this type. It appears in the Truth API only as a
+ * parameter to methods like {@link Subject#failWithActual(Fact, Fact...)}, which are used only by
+ * custom {@code Subject} implementations.
+ *
+ * <p>If you are writing a custom {@code Subject}, see <a
+ * href="https://truth.dev/failure_messages">our tips on writing failure messages</a>.
+ */
+public final class Fact implements Serializable {
+ /**
+ * Creates a fact with the given key and value, which will be printed in a format like "key:
+ * value." The value is converted to a string by calling {@code String.valueOf} on it.
+ */
+ public static Fact fact(String key, @Nullable Object value) {
+ return new Fact(key, String.valueOf(value));
+ }
+
+ /**
+ * Creates a fact with no value, which will be printed in the format "key" (with no colon or
+ * value).
+ *
+ * <p>In most cases, prefer {@linkplain #fact key-value facts}, which give Truth more flexibility
+ * in how to format the fact for display. {@code simpleFact} is useful primarily for:
+ *
+ * <ul>
+ * <li>messages from no-arg assertions. For example, {@code isNotEmpty()} would generate the
+ * fact "expected not to be empty"
+ * <li>prose that is part of a larger message. For example, {@code contains()} sometimes
+ * displays facts like "expected to contain: ..." <i>"but did not"</i> "though it did
+ * contain: ..."
+ * </ul>
+ */
+ public static Fact simpleFact(String key) {
+ return new Fact(key, null);
+ }
+
+ final String key;
+ final @Nullable String value;
+
+ private Fact(String key, @Nullable String value) {
+ this.key = checkNotNull(key);
+ this.value = value;
+ }
+
+ /**
+ * Returns a simple string representation for the fact. While this is used in the output of {@code
+ * TruthFailureSubject}, it's not used in normal failure messages, which automatically align facts
+ * horizontally and indent multiline values.
+ */
+ @Override
+ public String toString() {
+ return value == null ? key : key + ": " + value;
+ }
+
+ /**
+ * Formats the given messages and facts into a string for use as the message of a test failure. In
+ * particular, this method horizontally aligns the beginning of fact values.
+ */
+ static String makeMessage(ImmutableList<String> messages, ImmutableList<Fact> facts) {
+ int longestKeyLength = 0;
+ boolean seenNewlineInValue = false;
+ for (Fact fact : facts) {
+ if (fact.value != null) {
+ longestKeyLength = max(longestKeyLength, fact.key.length());
+ // TODO(cpovirk): Look for other kinds of newlines.
+ seenNewlineInValue |= fact.value.contains("\n");
+ }
+ }
+
+ StringBuilder builder = new StringBuilder();
+ for (String message : messages) {
+ builder.append(message);
+ builder.append('\n');
+ }
+
+ /*
+ * *Usually* the first fact is printed at the beginning of a new line. However, when this
+ * exception is the cause of another exception, that exception will print it starting after
+ * "Caused by: " on the same line. The other exception sometimes also reuses this message as its
+ * own message. In both of those scenarios, the first line doesn't start at column 0, so the
+ * horizontal alignment is thrown off.
+ *
+ * There's not much we can do about this, short of always starting with a newline (which would
+ * leave a blank line at the beginning of the message in the normal case).
+ */
+ for (Fact fact : facts) {
+ if (fact.value == null) {
+ builder.append(fact.key);
+ } else if (seenNewlineInValue) {
+ builder.append(fact.key);
+ builder.append(":\n");
+ builder.append(indent(fact.value));
+ } else {
+ builder.append(padEnd(fact.key, longestKeyLength, ' '));
+ builder.append(": ");
+ builder.append(fact.value);
+ }
+ builder.append('\n');
+ }
+ if (builder.length() > 0) {
+ builder.setLength(builder.length() - 1); // remove trailing \n
+ }
+ return builder.toString();
+ }
+
+ private static String indent(String value) {
+ // We don't want to indent with \t because the text would align exactly with the stack trace.
+ // We don't want to indent with \t\t because it would be very far for people with 8-space tabs.
+ // Let's compromise and indent by 4 spaces, which is different than both 2- and 8-space tabs.
+ return " " + value.replaceAll("\n", "\n ");
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/FailureMetadata.java b/core/src/main/java/com/google/common/truth/FailureMetadata.java
new file mode 100644
index 00000000..00c890cf
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/FailureMetadata.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Verify.verifyNotNull;
+import static com.google.common.truth.ComparisonFailures.makeComparisonFailureFacts;
+import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.LazyMessage.evaluateAll;
+import static com.google.common.truth.Platform.cleanStackTrace;
+import static com.google.common.truth.Platform.inferDescription;
+import static com.google.common.truth.Platform.makeComparisonFailure;
+import static com.google.common.truth.SubjectUtils.append;
+import static com.google.common.truth.SubjectUtils.concat;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * An opaque, immutable object containing state from the previous calls in the fluent assertion
+ * chain. It appears primarily as a parameter to {@link Subject} constructors (and {@link
+ * Subject.Factory} methods), which should pass it to the superclass constructor and not otherwise
+ * use or store it. In particular, users should not attempt to call {@code Subject} constructors or
+ * {@code Subject.Factory} methods directly. Instead, they should use the appropriate factory
+ * method:
+ *
+ * <ul>
+ * <li>If you're writing a test: {@link Truth#assertAbout(Subject.Factory)}{@code .that(...)}
+ * <li>If you're creating a derived subject from within another subject: {@code
+ * check(...).about(...).that(...)}
+ * <li>If you're testing your subject to verify that assertions fail when they should: {@link
+ * ExpectFailure}
+ * </ul>
+ *
+ * <p>(One exception: Implementations of {@link CustomSubjectBuilder} do directly call constructors,
+ * using their {@link CustomSubjectBuilder#metadata()} method to get an instance to pass to the
+ * constructor.)
+ */
+public final class FailureMetadata {
+ static FailureMetadata forFailureStrategy(FailureStrategy failureStrategy) {
+ return new FailureMetadata(
+ failureStrategy, ImmutableList.<LazyMessage>of(), ImmutableList.<Step>of());
+ }
+
+ private final FailureStrategy strategy;
+
+ /**
+ * The data from a call to either (a) a {@link Subject} constructor or (b) {@link Subject#check}.
+ */
+ private static final class Step {
+ static Step subjectCreation(Subject subject) {
+ return new Step(checkNotNull(subject), null, null);
+ }
+
+ static Step checkCall(
+ OldAndNewValuesAreSimilar valuesAreSimilar,
+ @Nullable Function<String, String> descriptionUpdate) {
+ return new Step(null, descriptionUpdate, valuesAreSimilar);
+ }
+
+ /*
+ * We store Subject, rather than the actual value itself, so that we can call
+ * actualCustomStringRepresentation(). Why not call actualCustomStringRepresentation()
+ * immediately? First, it might be expensive, and second, the Subject isn't initialized at the
+ * time we receive it. We *might* be able to make it safe to call if it looks only at actual(),
+ * but it might try to look at facts initialized by a subclass, which aren't ready yet.
+ */
+ final @Nullable Subject subject;
+
+ final @Nullable Function<String, String> descriptionUpdate;
+
+ // Present only when descriptionUpdate is.
+ final @Nullable OldAndNewValuesAreSimilar valuesAreSimilar;
+
+ private Step(
+ @Nullable Subject subject,
+ @Nullable Function<String, String> descriptionUpdate,
+ @Nullable OldAndNewValuesAreSimilar valuesAreSimilar) {
+ this.subject = subject;
+ this.descriptionUpdate = descriptionUpdate;
+ this.valuesAreSimilar = valuesAreSimilar;
+ }
+
+ boolean isCheckCall() {
+ return subject == null;
+ }
+ }
+
+ /*
+ * TODO(cpovirk): This implementation is wasteful, especially because `steps` is used even by
+ * non-chaining assertions. If it ever does matter, we could use an immutable cactus stack -- or
+ * probably even avoid storing most of the chain entirely (unless we end up wanting more of the
+ * chain to show "telescoping context," as in "the int value of this optional in this list in this
+ * multimap").
+ */
+
+ private final ImmutableList<LazyMessage> messages;
+
+ private final ImmutableList<Step> steps;
+
+ FailureMetadata(
+ FailureStrategy strategy, ImmutableList<LazyMessage> messages, ImmutableList<Step> steps) {
+ this.strategy = checkNotNull(strategy);
+ this.messages = checkNotNull(messages);
+ this.steps = checkNotNull(steps);
+ }
+
+ /**
+ * Returns a new instance that includes the given subject in its chain of values. Truth users do
+ * not need to call this method directly; Truth automatically accumulates context, starting from
+ * the initial that(...) call and continuing into any chained calls, like {@link
+ * ThrowableSubject#hasMessageThat}.
+ */
+ FailureMetadata updateForSubject(Subject subject) {
+ ImmutableList<Step> steps = append(this.steps, Step.subjectCreation(subject));
+ return derive(messages, steps);
+ }
+
+ FailureMetadata updateForCheckCall() {
+ ImmutableList<Step> steps = append(this.steps, Step.checkCall(null, null));
+ return derive(messages, steps);
+ }
+
+ FailureMetadata updateForCheckCall(
+ OldAndNewValuesAreSimilar valuesAreSimilar, Function<String, String> descriptionUpdate) {
+ checkNotNull(descriptionUpdate);
+ ImmutableList<Step> steps =
+ append(this.steps, Step.checkCall(valuesAreSimilar, descriptionUpdate));
+ return derive(messages, steps);
+ }
+
+ /**
+ * Whether the value of the original subject and the value of the derived subject are "similar
+ * enough" that we don't need to display both. For example, if we're printing a message about the
+ * value of optional.get(), there's no need to print the optional itself because it adds no
+ * information. Similarly, if we're printing a message about the asList() view of an array,
+ * there's no need to also print the array.
+ */
+ enum OldAndNewValuesAreSimilar {
+ SIMILAR,
+ DIFFERENT;
+ }
+
+ /**
+ * Returns a new instance whose failures will contain the given message. The way for Truth users
+ * to set a message is {@code check(...).withMessage(...).that(...)} (for calls from within a
+ * {@code Subject}) or {@link Truth#assertWithMessage} (for most other calls).
+ */
+ FailureMetadata withMessage(String format, /*@Nullable*/ Object[] args) {
+ ImmutableList<LazyMessage> messages = append(this.messages, new LazyMessage(format, args));
+ return derive(messages, steps);
+ }
+
+ void failEqualityCheck(
+ ImmutableList<Fact> headFacts,
+ ImmutableList<Fact> tailFacts,
+ String expected,
+ String actual) {
+ doFail(
+ makeComparisonFailure(
+ evaluateAll(messages),
+ makeComparisonFailureFacts(
+ concat(description(), headFacts),
+ concat(tailFacts, rootUnlessThrowable()),
+ expected,
+ actual),
+ expected,
+ actual,
+ rootCause()));
+ }
+
+ void fail(ImmutableList<Fact> facts) {
+ doFail(
+ new AssertionErrorWithFacts(
+ evaluateAll(messages),
+ concat(description(), facts, rootUnlessThrowable()),
+ rootCause()));
+ }
+
+ private void doFail(AssertionError failure) {
+ cleanStackTrace(failure);
+ strategy.fail(failure);
+ }
+
+ private FailureMetadata derive(ImmutableList<LazyMessage> messages, ImmutableList<Step> steps) {
+ return new FailureMetadata(strategy, messages, steps);
+ }
+
+ /**
+ * Returns a description of the final actual value, if it appears "interesting" enough to show.
+ * The description is considered interesting if the chain of derived subjects ends with at least
+ * one derivation that we have a name for. It's also considered interesting in the absence of
+ * derived subjects if we inferred a name for the root actual value from the bytecode.
+ *
+ * <p>We don't want to say: "value of string: expected [foo] but was [bar]" (OK, we might still
+ * decide to say this, but for now, we don't.)
+ *
+ * <p>We do want to say: "value of throwable.getMessage(): expected [foo] but was [bar]"
+ *
+ * <p>We also want to say: "value of getLogMessages(): expected not to be empty"
+ *
+ * <p>To support that, {@code descriptionIsInteresting} tracks whether we've been given context
+ * through {@code check} calls <i>that include names</i> or, initially, whether we inferred a name
+ * for the root actual value from the bytecode.
+ *
+ * <p>If we're missing a naming function halfway through, we have to reset: We don't want to claim
+ * that the value is "foo.bar.baz" when it's "foo.bar.somethingelse.baz." We have to go back to
+ * "object.baz." (But note that {@link #rootUnlessThrowable} will still provide the value of the
+ * root foo to the user as long as we had at least one naming function: We might not know the
+ * root's exact relationship to the final object, but we know it's some object "different enough"
+ * to be worth displaying.)
+ */
+ private ImmutableList<Fact> description() {
+ String description = inferDescription();
+ boolean descriptionIsInteresting = description != null;
+ for (Step step : steps) {
+ if (step.isCheckCall()) {
+ checkState(description != null);
+ if (step.descriptionUpdate == null) {
+ description = null;
+ descriptionIsInteresting = false;
+ } else {
+ description = verifyNotNull(step.descriptionUpdate.apply(description));
+ descriptionIsInteresting = true;
+ }
+ continue;
+ }
+
+ if (description == null) {
+ description = step.subject.typeDescription();
+ }
+ }
+ return descriptionIsInteresting
+ ? ImmutableList.of(fact("value of", description))
+ : ImmutableList.<Fact>of();
+ }
+
+ /**
+ * Returns the root actual value, if we know it's "different enough" from the final actual value.
+ *
+ * <p>We don't want to say: "expected [foo] but was [bar]. string: [bar]"
+ *
+ * <p>We do want to say: "expected [foo] but was [bar]. myObject: MyObject[string=bar, i=0]"
+ *
+ * <p>To support that, {@code seenDerivation} tracks whether we've seen multiple actual values,
+ * which is equivalent to whether we've seen multiple Subject instances or, more informally,
+ * whether the user is making a chained assertion.
+ *
+ * <p>There's one wrinkle: Sometimes chaining doesn't add information. This is often true with
+ * "internal" chaining, like when StreamSubject internally creates an IterableSubject to delegate
+ * to. The two subjects' string representations will be identical (or, in some cases, _almost_
+ * identical), so there is no value in showing both. In such cases, implementations can call the
+ * no-arg {@code checkNoNeedToDisplayBothValues()}, which sets {@code valuesAreSimilar},
+ * instructing this method that that particular chain link "doesn't count." (Note also that there
+ * are some edge cases that we're not sure how to handle yet, for which we might introduce
+ * additional {@code check}-like methods someday.)
+ */
+ // TODO(b/134505914): Consider returning multiple facts in some cases.
+ private ImmutableList<Fact> rootUnlessThrowable() {
+ Step rootSubject = null;
+ boolean seenDerivation = false;
+ for (Step step : steps) {
+ if (step.isCheckCall()) {
+ /*
+ * If we don't have a description update, don't trigger display of a root object. (If we
+ * did, we'd change the messages of a bunch of existing subjects, and we don't want to bite
+ * that off yet.)
+ *
+ * If we do have a description update, then trigger display of a root object but only if the
+ * old and new values are "different enough" to be worth both displaying.
+ */
+ seenDerivation |=
+ step.descriptionUpdate != null
+ && step.valuesAreSimilar == OldAndNewValuesAreSimilar.DIFFERENT;
+ continue;
+ }
+
+ if (rootSubject == null) {
+ if (step.subject.actual() instanceof Throwable) {
+ /*
+ * We'll already include the Throwable as a cause of the AssertionError (see rootCause()),
+ * so we don't need to include it again in the message.
+ */
+ return ImmutableList.of();
+ }
+ rootSubject = step;
+ }
+ }
+ /*
+ * TODO(cpovirk): Maybe say "root foo was: ..." instead of just "foo was: ..." if there's more
+ * than one foo in the chain, if the description string doesn't start with "foo," and/or if the
+ * name we have is just "object?"
+ */
+ return seenDerivation
+ ? ImmutableList.of(
+ fact(
+ // TODO(cpovirk): Use inferDescription() here when appropriate? But it can be long.
+ rootSubject.subject.typeDescription() + " was",
+ rootSubject.subject.actualCustomStringRepresentationForPackageMembersToCall()))
+ : ImmutableList.<Fact>of();
+ }
+
+ /**
+ * Returns the first {@link Throwable} in the chain of actual values. Typically, we'll have a root
+ * cause only if the assertion chain contains a {@link ThrowableSubject}.
+ */
+ private @Nullable Throwable rootCause() {
+ for (Step step : steps) {
+ if (!step.isCheckCall() && step.subject.actual() instanceof Throwable) {
+ return (Throwable) step.subject.actual();
+ }
+ }
+ return null;
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/FailureStrategy.java b/core/src/main/java/com/google/common/truth/FailureStrategy.java
new file mode 100644
index 00000000..81f1092a
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/FailureStrategy.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+/**
+ * Defines what to do when a check fails.
+ *
+ * <p>This type does not appear directly in a fluent assertion chain, but you choose a {@code
+ * FailureStrategy} by choosing which method to call at the beginning of the chain.
+ *
+ * <p>Built-in strategies include:
+ *
+ * <ul>
+ * <li>{@linkplain Truth#assert_ assertions}
+ * <li>{@linkplain Expect expectations}
+ * <li>{@linkplain TruthJUnit#assume assumptions}
+ * <li>(and some useful only to people who implement custom subjects, described below)
+ * </ul>
+ *
+ * <p>For more information about the fluent chain, see <a href="https://truth.dev/faq#full-chain">this
+ * FAQ entry</a>.
+ *
+ * <h3>For people extending Truth</h3>
+ *
+ * <p>Custom {@code FailureStrategy} implementations are unusual. If you think you need one,
+ * consider these alternatives:
+ *
+ * <ul>
+ * <li>To test a custom subject, use {@link ExpectFailure}.
+ * <li>To create subjects for other objects related to your actual value (for chained assertions),
+ * use {@link Subject#check(String, Object...)}, which preserves the existing {@code
+ * FailureStrategy} and other context.
+ * <li>To return a no-op subject after a previous assertion has failed (for chained assertions),
+ * use {@link Subject#ignoreCheck}
+ * </ul>
+ *
+ * <p>When you really do need to create your own strategy, rather than expose your {@code
+ * FailureStrategy} instance to users, expose a {@link StandardSubjectBuilder} instance using {@link
+ * StandardSubjectBuilder#forCustomFailureStrategy
+ * StandardSubjectBuilder.forCustomFailureStrategy(STRATEGY)}.
+ */
+public interface FailureStrategy {
+ /**
+ * Handles a failure. The parameter is an {@code AssertionError} or subclass thereof, and it
+ * contains information about the failure, which may include:
+ *
+ * <ul>
+ * <li>message: {@link Throwable#getMessage getMessage()}
+ * <li>cause: {@link Throwable#getCause getCause()}
+ * <li>actual and expected values: {@link org.junit.ComparisonFailure#getActual}, {@link
+ * org.junit.ComparisonFailure#getExpected}
+ * <li>stack trace: {@link Throwable#getStackTrace}
+ * </ul>
+ *
+ * <!-- TODO(cpovirk): suppressed exceptions someday? -->
+ *
+ * <p>We encourage implementations to record as much of this information as practical in the
+ * exceptions they may throw or the other records they may make.
+ */
+ void fail(AssertionError failure);
+}
diff --git a/core/src/main/java/com/google/common/truth/FloatSubject.java b/core/src/main/java/com/google/common/truth/FloatSubject.java
new file mode 100644
index 00000000..765b3844
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/FloatSubject.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.Fact.simpleFact;
+import static com.google.common.truth.MathUtil.equalWithinTolerance;
+import static com.google.common.truth.MathUtil.notEqualWithinTolerance;
+import static com.google.common.truth.Platform.floatToString;
+import static java.lang.Float.NaN;
+import static java.lang.Float.floatToIntBits;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Propositions for {@link Float} subjects.
+ *
+ * @author Kurt Alfred Kluever
+ */
+public final class FloatSubject extends ComparableSubject<Float> {
+ private static final int NEG_ZERO_BITS = floatToIntBits(-0.0f);
+
+ private final Float actual;
+ private final DoubleSubject asDouble;
+
+ FloatSubject(FailureMetadata metadata, @Nullable Float actual) {
+ super(metadata, actual);
+ this.actual = actual;
+ /*
+ * Doing anything with the FailureMetadata besides passing it to super(...) is generally bad
+ * practice. For an explanation of why it works out OK here, see LiteProtoSubject.
+ *
+ * An alternative approach would be to reimplement isLessThan, etc. here.
+ */
+ this.asDouble = new DoubleSubject(metadata, actual == null ? null : Double.valueOf(actual));
+ }
+
+ /**
+ * A partially specified check about an approximate relationship to a {@code float} subject using
+ * a tolerance.
+ */
+ public abstract static class TolerantFloatComparison {
+
+ // Prevent subclassing outside of this class
+ private TolerantFloatComparison() {}
+
+ /**
+ * Fails if the subject was expected to be within the tolerance of the given value but was not
+ * <i>or</i> if it was expected <i>not</i> to be within the tolerance but was. The subject and
+ * tolerance are specified earlier in the fluent call chain.
+ */
+ public abstract void of(float expectedFloat);
+
+ /**
+ * @throws UnsupportedOperationException always
+ * @deprecated {@link Object#equals(Object)} is not supported on TolerantFloatComparison. If you
+ * meant to compare floats, use {@link #of(float)} instead.
+ */
+ @Deprecated
+ @Override
+ public boolean equals(@Nullable Object o) {
+ throw new UnsupportedOperationException(
+ "If you meant to compare floats, use .of(float) instead.");
+ }
+
+ /**
+ * @throws UnsupportedOperationException always
+ * @deprecated {@link Object#hashCode()} is not supported on TolerantFloatComparison
+ */
+ @Deprecated
+ @Override
+ public int hashCode() {
+ throw new UnsupportedOperationException("Subject.hashCode() is not supported.");
+ }
+ }
+
+ /**
+ * Prepares for a check that the subject is a finite number within the given tolerance of an
+ * expected value that will be provided in the next call in the fluent chain.
+ *
+ * <p>The check will fail if either the subject or the object is {@link Float#POSITIVE_INFINITY},
+ * {@link Float#NEGATIVE_INFINITY}, or {@link Float#NaN}. To check for those values, use {@link
+ * #isPositiveInfinity}, {@link #isNegativeInfinity}, {@link #isNaN}, or (with more generality)
+ * {@link #isEqualTo}.
+ *
+ * <p>The check will pass if both values are zero, even if one is {@code 0.0f} and the other is
+ * {@code -0.0f}. Use {@code #isEqualTo} to assert that a value is exactly {@code 0.0f} or that it
+ * is exactly {@code -0.0f}.
+ *
+ * <p>You can use a tolerance of {@code 0.0f} to assert the exact equality of finite floats, but
+ * often {@link #isEqualTo} is preferable (note the different behaviours around non-finite values
+ * and {@code -0.0f}). See the documentation on {@link #isEqualTo} for advice on when exact
+ * equality assertions are appropriate.
+ *
+ * @param tolerance an inclusive upper bound on the difference between the subject and object
+ * allowed by the check, which must be a non-negative finite value, i.e. not {@link
+ * Float#NaN}, {@link Float#POSITIVE_INFINITY}, or negative, including {@code -0.0f}
+ */
+ public TolerantFloatComparison isWithin(final float tolerance) {
+ return new TolerantFloatComparison() {
+ @Override
+ public void of(float expected) {
+ Float actual = FloatSubject.this.actual;
+ checkNotNull(
+ actual, "actual value cannot be null. tolerance=%s expected=%s", tolerance, expected);
+ checkTolerance(tolerance);
+
+ if (!equalWithinTolerance(actual, expected, tolerance)) {
+ failWithoutActual(
+ fact("expected", floatToString(expected)),
+ butWas(),
+ fact("outside tolerance", floatToString(tolerance)));
+ }
+ }
+ };
+ }
+
+ /**
+ * Prepares for a check that the subject is a finite number not within the given tolerance of an
+ * expected value that will be provided in the next call in the fluent chain.
+ *
+ * <p>The check will fail if either the subject or the object is {@link Float#POSITIVE_INFINITY},
+ * {@link Float#NEGATIVE_INFINITY}, or {@link Float#NaN}. See {@link #isFinite}, {@link
+ * #isNotNaN}, or {@link #isNotEqualTo} for checks with other behaviours.
+ *
+ * <p>The check will fail if both values are zero, even if one is {@code 0.0f} and the other is
+ * {@code -0.0f}. Use {@code #isNotEqualTo} for a test which fails for a value of exactly zero
+ * with one sign but passes for zero with the opposite sign.
+ *
+ * <p>You can use a tolerance of {@code 0.0f} to assert the exact non-equality of finite floats,
+ * but sometimes {@link #isNotEqualTo} is preferable (note the different behaviours around
+ * non-finite values and {@code -0.0f}).
+ *
+ * @param tolerance an exclusive lower bound on the difference between the subject and object
+ * allowed by the check, which must be a non-negative finite value, i.e. not {@code
+ * Float.NaN}, {@code Float.POSITIVE_INFINITY}, or negative, including {@code -0.0f}
+ */
+ public TolerantFloatComparison isNotWithin(final float tolerance) {
+ return new TolerantFloatComparison() {
+ @Override
+ public void of(float expected) {
+ Float actual = FloatSubject.this.actual;
+ checkNotNull(
+ actual, "actual value cannot be null. tolerance=%s expected=%s", tolerance, expected);
+ checkTolerance(tolerance);
+
+ if (!notEqualWithinTolerance(actual, expected, tolerance)) {
+ failWithoutActual(
+ fact("expected not to be", floatToString(expected)),
+ butWas(),
+ fact("within tolerance", floatToString(tolerance)));
+ }
+ }
+ };
+ }
+
+ /**
+ * Asserts that the subject is exactly equal to the given value, with equality defined as by
+ * {@code Float#equals}. This method is <i>not</i> recommended when the code under test is doing
+ * any kind of arithmetic: use {@link #isWithin} with a suitable tolerance in that case. (Remember
+ * that the exact result of floating point arithmetic is sensitive to apparently trivial changes
+ * such as replacing {@code (a + b) + c} with {@code a + (b + c)}, and that unless {@code
+ * strictfp} is in force even the result of {@code (a + b) + c} is sensitive to the JVM's choice
+ * of precision for the intermediate result.) This method is recommended when the code under test
+ * is specified as either copying a value without modification from its input or returning a
+ * well-defined literal or constant value.
+ *
+ * <p><b>Note:</b> The assertion {@code isEqualTo(0.0f)} fails for an input of {@code -0.0f}, and
+ * vice versa. For an assertion that passes for either {@code 0.0f} or {@code -0.0f}, use {@link
+ * #isZero}.
+ */
+ @Override
+ public final void isEqualTo(@Nullable Object other) {
+ super.isEqualTo(other);
+ }
+
+ /**
+ * Asserts that the subject is not exactly equal to the given value, with equality defined as by
+ * {@code Float#equals}. See {@link #isEqualTo} for advice on when exact equality is recommended.
+ * Use {@link #isNotWithin} for an assertion with a tolerance.
+ *
+ * <p><b>Note:</b> The assertion {@code isNotEqualTo(0.0f)} passes for {@code -0.0f}, and vice
+ * versa. For an assertion that fails for either {@code 0.0f} or {@code -0.0f}, use {@link
+ * #isNonZero}.
+ */
+ @Override
+ public final void isNotEqualTo(@Nullable Object other) {
+ super.isNotEqualTo(other);
+ }
+
+ /**
+ * @deprecated Use {@link #isWithin} or {@link #isEqualTo} instead (see documentation for advice).
+ */
+ @Override
+ @Deprecated
+ public final void isEquivalentAccordingToCompareTo(Float other) {
+ super.isEquivalentAccordingToCompareTo(other);
+ }
+
+ /**
+ * Ensures that the given tolerance is a non-negative finite value, i.e. not {@code Float.NaN},
+ * {@code Float.POSITIVE_INFINITY}, or negative, including {@code -0.0f}.
+ */
+ static void checkTolerance(float tolerance) {
+ checkArgument(!Float.isNaN(tolerance), "tolerance cannot be NaN");
+ checkArgument(tolerance >= 0.0f, "tolerance (%s) cannot be negative", tolerance);
+ checkArgument(
+ floatToIntBits(tolerance) != NEG_ZERO_BITS, "tolerance (%s) cannot be negative", tolerance);
+ checkArgument(tolerance != Float.POSITIVE_INFINITY, "tolerance cannot be POSITIVE_INFINITY");
+ }
+
+ /** Asserts that the subject is zero (i.e. it is either {@code 0.0f} or {@code -0.0f}). */
+ public final void isZero() {
+ if (actual == null || actual.floatValue() != 0.0f) {
+ failWithActual(simpleFact("expected zero"));
+ }
+ }
+
+ /**
+ * Asserts that the subject is a non-null value other than zero (i.e. it is not {@code 0.0f},
+ * {@code -0.0f} or {@code null}).
+ */
+ public final void isNonZero() {
+ if (actual == null) {
+ failWithActual(simpleFact("expected a float other than zero"));
+ } else if (actual.floatValue() == 0.0f) {
+ failWithActual(simpleFact("expected not to be zero"));
+ }
+ }
+
+ /** Asserts that the subject is {@link Float#POSITIVE_INFINITY}. */
+ public final void isPositiveInfinity() {
+ isEqualTo(Float.POSITIVE_INFINITY);
+ }
+
+ /** Asserts that the subject is {@link Float#NEGATIVE_INFINITY}. */
+ public final void isNegativeInfinity() {
+ isEqualTo(Float.NEGATIVE_INFINITY);
+ }
+
+ /** Asserts that the subject is {@link Float#NaN}. */
+ public final void isNaN() {
+ isEqualTo(NaN);
+ }
+
+ /**
+ * Asserts that the subject is finite, i.e. not {@link Float#POSITIVE_INFINITY}, {@link
+ * Float#NEGATIVE_INFINITY}, or {@link Float#NaN}.
+ */
+ public final void isFinite() {
+ if (actual == null || actual.isNaN() || actual.isInfinite()) {
+ failWithActual(simpleFact("expected to be finite"));
+ }
+ }
+
+ /**
+ * Asserts that the subject is a non-null value other than {@link Float#NaN} (but it may be {@link
+ * Float#POSITIVE_INFINITY} or {@link Float#NEGATIVE_INFINITY}).
+ */
+ public final void isNotNaN() {
+ if (actual == null) {
+ failWithActual(simpleFact("expected a float other than NaN"));
+ } else {
+ isNotEqualTo(NaN);
+ }
+ }
+
+ /**
+ * Checks that the subject is greater than {@code other}.
+ *
+ * <p>To check that the subject is greater than <i>or equal to</i> {@code other}, use {@link
+ * #isAtLeast}.
+ */
+ public final void isGreaterThan(int other) {
+ asDouble.isGreaterThan(other);
+ }
+
+ /**
+ * Checks that the subject is less than {@code other}.
+ *
+ * <p>To check that the subject is less than <i>or equal to</i> {@code other}, use {@link
+ * #isAtMost} .
+ */
+ public final void isLessThan(int other) {
+ asDouble.isLessThan(other);
+ }
+
+ /**
+ * Checks that the subject is less than or equal to {@code other}.
+ *
+ * <p>To check that the subject is <i>strictly</i> less than {@code other}, use {@link
+ * #isLessThan}.
+ */
+ public final void isAtMost(int other) {
+ asDouble.isAtMost(other);
+ }
+
+ /**
+ * Checks that the subject is greater than or equal to {@code other}.
+ *
+ * <p>To check that the subject is <i>strictly</i> greater than {@code other}, use {@link
+ * #isGreaterThan}.
+ */
+ public final void isAtLeast(int other) {
+ asDouble.isAtLeast(other);
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/GraphMatching.java b/core/src/main/java/com/google/common/truth/GraphMatching.java
new file mode 100644
index 00000000..6a266f5d
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/GraphMatching.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import com.google.common.collect.ImmutableBiMap;
+import com.google.common.collect.Multimap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.util.ArrayDeque;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Queue;
+
+/**
+ * Helper routines related to <a href="https://en.wikipedia.org/wiki/Matching_(graph_theory)">graph
+ * matchings</a>.
+ *
+ * @author Pete Gillin
+ */
+final class GraphMatching {
+
+ /**
+ * Finds a <a
+ * href="https://en.wikipedia.org/wiki/Matching_(graph_theory)#In_unweighted_bipartite_graphs">
+ * maximum cardinality matching of a bipartite graph</a>. The vertices of one part of the
+ * bipartite graph are identified by objects of type {@code U} using object equality. The vertices
+ * of the other part are similarly identified by objects of type {@code V}. The input bipartite
+ * graph is represented as a {@code Multimap<U, V>}: each entry represents an edge, with the key
+ * representing the vertex in the first part and the value representing the value in the second
+ * part. (Note that, even if {@code U} and {@code V} are the same type, equality between a key and
+ * a value has no special significance: effectively, they are in different domains.) Fails if any
+ * of the vertices (keys or values) are null. The output matching is similarly represented as a
+ * {@code BiMap<U, V>} (the property that a matching has no common vertices translates into the
+ * bidirectional uniqueness property of the {@link BiMap}).
+ *
+ * <p>If there are multiple matchings which share the maximum cardinality, an arbitrary one is
+ * returned.
+ */
+ static <U, V> ImmutableBiMap<U, V> maximumCardinalityBipartiteMatching(Multimap<U, V> graph) {
+ return HopcroftKarp.overBipartiteGraph(graph).perform();
+ }
+
+ private GraphMatching() {}
+
+ /**
+ * Helper which implements the <a
+ * href="https://en.wikipedia.org/wiki/Hopcroft%E2%80%93Karp_algorithm">Hopcroft–Karp</a>
+ * algorithm.
+ *
+ * <p>The worst-case complexity is {@code O(E V^0.5)} where the graph contains {@code E} edges and
+ * {@code V} vertices. For dense graphs, where {@code E} is {@code O(V^2)}, this is {@code V^2.5}
+ * (and non-dense graphs perform better than dense graphs with the same number of vertices).
+ */
+ private static class HopcroftKarp<U, V> {
+
+ private final Multimap<U, V> graph;
+
+ /**
+ * Factory method which returns an instance ready to perform the algorithm over the bipartite
+ * graph described by the given multimap.
+ */
+ static <U, V> HopcroftKarp<U, V> overBipartiteGraph(Multimap<U, V> graph) {
+ return new HopcroftKarp<>(graph);
+ }
+
+ private HopcroftKarp(Multimap<U, V> graph) {
+ this.graph = graph;
+ }
+
+ /** Performs the algorithm, and returns a bimap describing the matching found. */
+ ImmutableBiMap<U, V> perform() {
+ BiMap<U, V> matching = HashBiMap.create();
+ while (true) {
+ // Perform the BFS as described below. This finds the length of the shortest augmenting path
+ // and a guide which locates all the augmenting paths of that length.
+ Map<U, Integer> layers = new HashMap<>();
+ Optional<Integer> freeRhsVertexLayer = breadthFirstSearch(matching, layers);
+ if (!freeRhsVertexLayer.isPresent()) {
+ // The BFS failed, i.e. we found no augmenting paths. So we're done.
+ break;
+ }
+ // Perform the DFS and update the matching as described below starting from each free LHS
+ // vertex. This finds a disjoint set of augmenting paths of the shortest length and updates
+ // the matching by computing the symmetric difference with that set.
+ for (U lhs : graph.keySet()) {
+ if (!matching.containsKey(lhs)) {
+ depthFirstSearch(matching, layers, freeRhsVertexLayer.get(), lhs);
+ }
+ }
+ }
+ return ImmutableBiMap.copyOf(matching);
+ }
+
+ /**
+ * Performs the Breadth-First Search phase of the algorithm. Specifically, treats the bipartite
+ * graph as a directed graph where every unmatched edge (i.e. every edge not in the current
+ * matching) is directed from the LHS vertex to the RHS vertex and every matched edge is
+ * directed from the RHS vertex to the LHS vertex, and performs a BFS which starts from all of
+ * the free LHS vertices (i.e. the LHS vertices which are not in the current matching) and stops
+ * either at the end of a layer where a free RHS vertex is found or when the search is exhausted
+ * if no free RHS vertex is found. Keeps track of which layer of the BFS each LHS vertex was
+ * found in (for those LHS vertices visited during the BFS), so the free LHS vertices are in
+ * layer 1, those reachable by following an unmatched edge from any free LHS vertex to any
+ * non-free RHS vertex and then the matched edge back to a LHS vertex are in layer 2, etc. Note
+ * that every path in a successful search starts with a free LHS vertex and ends with a free RHS
+ * vertex, with every intermediate vertex being non-free.
+ *
+ * @param matching A bimap describing the matching to be used for the BFS, which is not modified
+ * by this method
+ * @param layers A map to be filled with the layer of each LHS vertex visited during the BFS,
+ * which should be empty when passed into this method and will be modified by this method
+ * @return The number of the layer in which the first free RHS vertex was found, if any, and the
+ * absent value if the BFS was exhausted without finding any free RHS vertex
+ */
+ private Optional<Integer> breadthFirstSearch(BiMap<U, V> matching, Map<U, Integer> layers) {
+ Queue<U> queue = new ArrayDeque<>();
+ Optional<Integer> freeRhsVertexLayer = Optional.absent();
+
+ // Enqueue all free LHS vertices and assign them to layer 1.
+ for (U lhs : graph.keySet()) {
+ if (!matching.containsKey(lhs)) {
+ layers.put(lhs, 1);
+ queue.add(lhs);
+ }
+ }
+
+ // Now proceed with the BFS.
+ while (!queue.isEmpty()) {
+ U lhs = queue.remove();
+ int layer = layers.get(lhs);
+ // If the BFS has proceeded past a layer in which a free RHS vertex was found, stop.
+ if (freeRhsVertexLayer.isPresent() && layer > freeRhsVertexLayer.get()) {
+ break;
+ }
+ // We want to consider all the unmatched edges from the current LHS vertex to the RHS, and
+ // then all the matched edges from those RHS vertices back to the LHS, to find the next
+ // layer of LHS vertices. We actually iterate over all edges, both matched and unmatched,
+ // from the current LHS vertex: we'll just do nothing for matched edges.
+ for (V rhs : graph.get(lhs)) {
+ if (!matching.containsValue(rhs)) {
+ // We found a free RHS vertex. Record the layer at which we found it. Since the RHS
+ // vertex is free, there is no matched edge to follow. (Note that the edge from the LHS
+ // to the RHS must be unmatched, because a matched edge cannot lead to a free vertex.)
+ if (!freeRhsVertexLayer.isPresent()) {
+ freeRhsVertexLayer = Optional.of(layer);
+ }
+ } else {
+ // We found an RHS vertex with a matched vertex back to the LHS. If we haven't visited
+ // that new LHS vertex yet, add it to the next layer. (If the edge from the LHS to the
+ // RHS was matched then the matched edge from the RHS to the LHS will lead back to the
+ // current LHS vertex, which has definitely been visited, so we correctly do nothing.)
+ U nextLhs = matching.inverse().get(rhs);
+ if (!layers.containsKey(nextLhs)) {
+ layers.put(nextLhs, layer + 1);
+ queue.add(nextLhs);
+ }
+ }
+ }
+ }
+
+ return freeRhsVertexLayer;
+ }
+
+ /**
+ * Performs the Depth-First Search phase of the algorithm. The DFS is guided by the BFS phase,
+ * i.e. it only uses paths which were used in the BFS. That means the steps in the DFS proceed
+ * from an LHS vertex via an unmatched edge to an RHS vertex and from an RHS vertex via a
+ * matched edge to an LHS vertex only if that LHS vertex is one layer deeper in the BFS than the
+ * previous one. It starts from the specified LHS vertex and stops either when it finds one of
+ * the free RHS vertices located by the BFS or when the search is exhausted. If a free RHS
+ * vertex is found then all the unmatched edges in the search path and added to the matching and
+ * all the matched edges in the search path are removed from the matching; in other words, the
+ * direction (which is determined by the matched/unmatched status) of every edge in the search
+ * path is flipped. Note several properties of this update to the matching:
+ *
+ * <ul>
+ * <li>Because the search path must contain one more unmatched than matched edges, the effect
+ * of this modification is to increase the size of the matching by one.
+ * <li>This modification results in the free LHS vertex at the start of the path and the free
+ * RHS vertex at the end of the path becoming non-free, while the intermediate non-free
+ * vertices stay non-free.
+ * <li>None of the edges used in this search path may be used in any further DFS. They cannot
+ * be used in the same direction as they were in this DFS because their directions are
+ * flipped; and they cannot be used in their new directions because we only use edges
+ * leading to the next layer of the BFS and, after flipping the directions, these edges
+ * now lead to the previous layer.
+ * <li>As a consequence of the previous property, repeated invocations of this method will
+ * find only paths which were used in the BFS and which were not used in any previous DFS
+ * (i.e. the set of edges used in the paths found by repeated DFSes are disjoint).
+ * </ul>
+ *
+ * @param matching A bimap describing the matching to be used for the BFS, which will be
+ * modified by this method as described above
+ * @param layers A map giving the layer of each LHS vertex visited during the BFS, which will
+ * not be modified by this method
+ * @param freeRhsVertexLayer The number of the layer in which the first free RHS vertex was
+ * found
+ * @param lhs The LHS vertex from which to start the DFS
+ * @return Whether or not the DFS was successful
+ */
+ @CanIgnoreReturnValue
+ private boolean depthFirstSearch(
+ BiMap<U, V> matching, Map<U, Integer> layers, int freeRhsVertexLayer, U lhs) {
+ // Note that this differs from the method described in the text of the wikipedia article (at
+ // time of writing) in two ways. Firstly, we proceed from a free LHS vertex to a free RHS
+ // vertex in the target layer instead of the other way around, which makes no difference.
+ // Secondly, we update the matching using the path found from each DFS after it is found,
+ // rather than using all the paths at the end of the phase. As explained above, the effect of
+ // this is that we automatically find only the disjoint set of paths, as required. This is,
+ // fact, the approach taken in the pseudocode of the wikipedia article (at time of writing).
+ int layer = layers.get(lhs);
+ if (layer > freeRhsVertexLayer) {
+ // We've gone past the target layer, so we're not going to find what we're looking for.
+ return false;
+ }
+ // Consider every edge from this LHS vertex.
+ for (V rhs : graph.get(lhs)) {
+ if (!matching.containsValue(rhs)) {
+ // We found a free RHS vertex. (This must have been in the target layer because, by
+ // definition, no free RHS vertex is reachable in any earlier layer, and because we stop
+ // when we get past that layer.) We add the unmatched edge used to get here to the
+ // matching, and remove any previous matched edge leading to the LHS vertex.
+ matching.forcePut(lhs, rhs);
+ return true;
+ } else {
+ // We found a non-free RHS vertex. Follow the matched edge from that RHS vertex to find
+ // the next LHS vertex.
+ U nextLhs = matching.inverse().get(rhs);
+ if (layers.containsKey(nextLhs) && layers.get(nextLhs) == layer + 1) {
+ // The next LHS vertex is in the next layer of the BFS, so we can use this path for our
+ // DFS. Recurse into the DFS.
+ if (depthFirstSearch(matching, layers, freeRhsVertexLayer, nextLhs)) {
+ // The DFS succeeded, and we're reversing back up the search path. At each stage we
+ // put the unmatched edge from the LHS to the RHS into the matching, and remove any
+ // matched edge previously leading to the LHS. The combined effect of all the
+ // modifications made while reversing all the way back up the search path is to update
+ // the matching as described in the javadoc.
+ matching.forcePut(lhs, rhs);
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/GuavaOptionalSubject.java b/core/src/main/java/com/google/common/truth/GuavaOptionalSubject.java
new file mode 100644
index 00000000..bbaf8053
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/GuavaOptionalSubject.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.Fact.simpleFact;
+
+import com.google.common.base.Optional;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Propositions for Guava {@link Optional} subjects.
+ *
+ * <p>If you are looking for a {@code java.util.Optional} subject, please read
+ * <a href="https://truth.dev/faq#java8">faq#java8</a>
+ *
+ * @author Christian Gruber
+ */
+public final class GuavaOptionalSubject extends Subject {
+ private final Optional<?> actual;
+
+ GuavaOptionalSubject(
+ FailureMetadata metadata, @Nullable Optional<?> actual, @Nullable String typeDescription) {
+ super(metadata, actual, typeDescription);
+ this.actual = actual;
+ }
+
+ /** Fails if the {@link Optional}{@code <T>} is absent or the subject is null. */
+ public void isPresent() {
+ if (actual == null) {
+ failWithActual(simpleFact("expected present optional"));
+ } else if (!actual.isPresent()) {
+ failWithoutActual(simpleFact("expected to be present"));
+ }
+ }
+
+ /** Fails if the {@link Optional}{@code <T>} is present or the subject is null. */
+ public void isAbsent() {
+ if (actual == null) {
+ failWithActual(simpleFact("expected absent optional"));
+ } else if (actual.isPresent()) {
+ failWithoutActual(
+ simpleFact("expected to be absent"), fact("but was present with value", actual.get()));
+ }
+ }
+
+ /**
+ * Fails if the {@link Optional}{@code <T>} does not have the given value or the subject is null.
+ *
+ * <p>To make more complex assertions on the optional's value split your assertion in two:
+ *
+ * <pre>{@code
+ * assertThat(myOptional).isPresent();
+ * assertThat(myOptional.get()).contains("foo");
+ * }</pre>
+ */
+ public void hasValue(Object expected) {
+ if (expected == null) {
+ throw new NullPointerException("Optional cannot have a null value.");
+ }
+ if (actual == null) {
+ failWithActual("expected an optional with value", expected);
+ } else if (!actual.isPresent()) {
+ failWithoutActual(fact("expected to have value", expected), simpleFact("but was absent"));
+ } else {
+ checkNoNeedToDisplayBothValues("get()").that(actual.get()).isEqualTo(expected);
+ }
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/IntegerSubject.java b/core/src/main/java/com/google/common/truth/IntegerSubject.java
new file mode 100644
index 00000000..2f892379
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/IntegerSubject.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Propositions for {@link Integer} subjects.
+ *
+ * @author David Saff
+ * @author Christian Gruber (cgruber@israfil.net)
+ * @author Kurt Alfred Kluever
+ */
+public class IntegerSubject extends ComparableSubject<Integer> {
+ /**
+ * Constructor for use by subclasses. If you want to create an instance of this class itself, call
+ * {@link Subject#check(String, Object...) check(...)}{@code .that(actual)}.
+ */
+ protected IntegerSubject(FailureMetadata metadata, @Nullable Integer integer) {
+ super(metadata, integer);
+ }
+
+ /** @deprecated Use {@link #isEqualTo} instead. Integer comparison is consistent with equality. */
+ @Override
+ @Deprecated
+ public final void isEquivalentAccordingToCompareTo(Integer other) {
+ super.isEquivalentAccordingToCompareTo(other);
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/IterableSubject.java b/core/src/main/java/com/google/common/truth/IterableSubject.java
new file mode 100644
index 00000000..bfb5dad2
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/IterableSubject.java
@@ -0,0 +1,2070 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Strings.lenientFormat;
+import static com.google.common.collect.Iterables.concat;
+import static com.google.common.collect.Iterables.size;
+import static com.google.common.collect.Lists.newArrayList;
+import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.Fact.simpleFact;
+import static com.google.common.truth.IterableSubject.ElementFactGrouping.ALL_IN_ONE_FACT;
+import static com.google.common.truth.IterableSubject.ElementFactGrouping.FACT_PER_ELEMENT;
+import static com.google.common.truth.SubjectUtils.accumulate;
+import static com.google.common.truth.SubjectUtils.annotateEmptyStrings;
+import static com.google.common.truth.SubjectUtils.countDuplicates;
+import static com.google.common.truth.SubjectUtils.countDuplicatesAndAddTypeInfo;
+import static com.google.common.truth.SubjectUtils.countDuplicatesAndMaybeAddTypeInfoReturnObject;
+import static com.google.common.truth.SubjectUtils.entryString;
+import static com.google.common.truth.SubjectUtils.hasMatchingToStringPair;
+import static com.google.common.truth.SubjectUtils.iterableToCollection;
+import static com.google.common.truth.SubjectUtils.iterableToList;
+import static com.google.common.truth.SubjectUtils.objectToTypeName;
+import static com.google.common.truth.SubjectUtils.retainMatchingToString;
+import static java.util.Arrays.asList;
+
+import com.google.common.base.Function;
+import com.google.common.base.Objects;
+import com.google.common.base.Optional;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.ImmutableBiMap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.LinkedHashMultiset;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multiset;
+import com.google.common.collect.Ordering;
+import com.google.common.collect.Sets;
+import com.google.common.truth.Correspondence.DiffFormatter;
+import com.google.common.truth.SubjectUtils.DuplicateGroupedAndTyped;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Propositions for {@link Iterable} subjects.
+ *
+ * <p><b>Note:</b>
+ *
+ * <ul>
+ * <li>Assertions may iterate through the given {@link Iterable} more than once. If you have an
+ * unusual implementation of {@link Iterable} which does not support multiple iterations
+ * (sometimes known as a "one-shot iterable"), you must copy your iterable into a collection
+ * which does (e.g. {@code ImmutableList.copyOf(iterable)} or, if your iterable may contain
+ * null, {@code newArrayList(iterable)}). If you don't, you may see surprising failures.
+ * <li>Assertions may also require that the elements in the given {@link Iterable} implement
+ * {@link Object#hashCode} correctly.
+ * </ul>
+ *
+ * @author Kurt Alfred Kluever
+ * @author Pete Gillin
+ */
+// Can't be final since MultisetSubject and SortedSetSubject extend it
+public class IterableSubject extends Subject {
+
+ private final Iterable<?> actual;
+
+ /**
+ * Constructor for use by subclasses. If you want to create an instance of this class itself, call
+ * {@link Subject#check(String, Object...) check(...)}{@code .that(actual)}.
+ */
+ protected IterableSubject(FailureMetadata metadata, @Nullable Iterable<?> iterable) {
+ super(metadata, iterable);
+ this.actual = iterable;
+ }
+
+ @Override
+ protected String actualCustomStringRepresentation() {
+ if (actual != null) {
+ // Check the value of iterable.toString() against the default Object.toString() implementation
+ // so we can avoid things like "com.google.common.graph.Traverser$GraphTraverser$1@5e316c74"
+ String objectToString =
+ actual.getClass().getName() + '@' + Integer.toHexString(System.identityHashCode(actual));
+ if (actual.toString().equals(objectToString)) {
+ return Iterables.toString(actual);
+ }
+ }
+ return super.actualCustomStringRepresentation();
+ }
+
+ @Override
+ public void isEqualTo(@Nullable Object expected) {
+ @SuppressWarnings("UndefinedEquals") // method contract requires testing iterables for equality
+ boolean equal = Objects.equal(actual, expected);
+ if (equal) {
+ return;
+ }
+
+ // Fail but with a more descriptive message:
+
+ if (actual instanceof List && expected instanceof List) {
+ containsExactlyElementsIn((List<?>) expected).inOrder();
+ } else if ((actual instanceof Set && expected instanceof Set)
+ || (actual instanceof Multiset && expected instanceof Multiset)) {
+ containsExactlyElementsIn((Collection<?>) expected);
+ } else {
+ /*
+ * TODO(b/18430105): Consider a special message if comparing incompatible collection types
+ * (similar to what MultimapSubject has).
+ */
+ super.isEqualTo(expected);
+ }
+ }
+
+ /** Fails if the subject is not empty. */
+ public final void isEmpty() {
+ if (!Iterables.isEmpty(actual)) {
+ failWithActual(simpleFact("expected to be empty"));
+ }
+ }
+
+ /** Fails if the subject is empty. */
+ public final void isNotEmpty() {
+ if (Iterables.isEmpty(actual)) {
+ failWithoutActual(simpleFact("expected not to be empty"));
+ }
+ }
+
+ /** Fails if the subject does not have the given size. */
+ public final void hasSize(int expectedSize) {
+ checkArgument(expectedSize >= 0, "expectedSize(%s) must be >= 0", expectedSize);
+ int actualSize = size(actual);
+ check("size()").that(actualSize).isEqualTo(expectedSize);
+ }
+
+ /** Checks (with a side-effect failure) that the subject contains the supplied item. */
+ public final void contains(@Nullable Object element) {
+ if (!Iterables.contains(actual, element)) {
+ List<Object> elementList = newArrayList(element);
+ if (hasMatchingToStringPair(actual, elementList)) {
+ failWithoutActual(
+ fact("expected to contain", element),
+ fact("an instance of", objectToTypeName(element)),
+ simpleFact("but did not"),
+ fact(
+ "though it did contain",
+ countDuplicatesAndAddTypeInfo(
+ retainMatchingToString(actual, elementList /* itemsToCheck */))),
+ fullContents());
+ } else {
+ failWithActual("expected to contain", element);
+ }
+ }
+ }
+
+ /** Checks (with a side-effect failure) that the subject does not contain the supplied item. */
+ public final void doesNotContain(@Nullable Object element) {
+ if (Iterables.contains(actual, element)) {
+ failWithActual("expected not to contain", element);
+ }
+ }
+
+ /** Checks that the subject does not contain duplicate elements. */
+ public final void containsNoDuplicates() {
+ List<Multiset.Entry<?>> duplicates = newArrayList();
+ for (Multiset.Entry<?> entry : LinkedHashMultiset.create(actual).entrySet()) {
+ if (entry.getCount() > 1) {
+ duplicates.add(entry);
+ }
+ }
+ if (!duplicates.isEmpty()) {
+ failWithoutActual(
+ simpleFact("expected not to contain duplicates"),
+ fact("but contained", duplicates),
+ fullContents());
+ }
+ }
+
+ /** Checks that the subject contains at least one of the provided objects or fails. */
+ public final void containsAnyOf(
+ @Nullable Object first, @Nullable Object second, @Nullable Object /*@Nullable*/... rest) {
+ containsAnyIn(accumulate(first, second, rest));
+ }
+
+ /**
+ * Checks that the subject contains at least one of the objects contained in the provided
+ * collection or fails.
+ */
+ // TODO(cpovirk): Consider using makeElementFacts-style messages here, in contains(), etc.
+ public final void containsAnyIn(Iterable<?> expected) {
+ Collection<?> actual = iterableToCollection(this.actual);
+ for (Object item : expected) {
+ if (actual.contains(item)) {
+ return;
+ }
+ }
+ if (hasMatchingToStringPair(actual, expected)) {
+ failWithoutActual(
+ fact("expected to contain any of", countDuplicatesAndAddTypeInfo(expected)),
+ simpleFact("but did not"),
+ fact(
+ "though it did contain",
+ countDuplicatesAndAddTypeInfo(
+ retainMatchingToString(this.actual, expected /* itemsToCheck */))),
+ fullContents());
+ } else {
+ failWithActual("expected to contain any of", expected);
+ }
+ }
+
+ /**
+ * Checks that the subject contains at least one of the objects contained in the provided array or
+ * fails.
+ */
+ public final void containsAnyIn(Object[] expected) {
+ containsAnyIn(asList(expected));
+ }
+
+ /**
+ * Checks that the actual iterable contains at least all of the expected elements or fails. If an
+ * element appears more than once in the expected elements to this call then it must appear at
+ * least that number of times in the actual elements.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method. The expected elements must appear in the given order
+ * within the actual elements, but they are not required to be consecutive.
+ */
+ @CanIgnoreReturnValue
+ public final Ordered containsAtLeast(
+ @Nullable Object firstExpected,
+ @Nullable Object secondExpected,
+ @Nullable Object /*@Nullable*/... restOfExpected) {
+ return containsAtLeastElementsIn(accumulate(firstExpected, secondExpected, restOfExpected));
+ }
+
+ /**
+ * Checks that the actual iterable contains at least all of the expected elements or fails. If an
+ * element appears more than once in the expected elements then it must appear at least that
+ * number of times in the actual elements.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method. The expected elements must appear in the given order
+ * within the actual elements, but they are not required to be consecutive.
+ */
+ @CanIgnoreReturnValue
+ public final Ordered containsAtLeastElementsIn(Iterable<?> expectedIterable) {
+ List<?> actual = Lists.newLinkedList(this.actual);
+ final Collection<?> expected = iterableToCollection(expectedIterable);
+
+ List<Object> missing = newArrayList();
+ List<Object> actualNotInOrder = newArrayList();
+
+ boolean ordered = true;
+ // step through the expected elements...
+ for (Object e : expected) {
+ int index = actual.indexOf(e);
+ if (index != -1) { // if we find the element in the actual list...
+ // drain all the elements that come before that element into actualNotInOrder
+ moveElements(actual, actualNotInOrder, index);
+ // and remove the element from the actual list
+ actual.remove(0);
+ } else { // otherwise try removing it from actualNotInOrder...
+ if (actualNotInOrder.remove(e)) { // if it was in actualNotInOrder, we're not in order
+ ordered = false;
+ } else { // if it's not in actualNotInOrder, we're missing an expected element
+ missing.add(e);
+ }
+ }
+ }
+ // if we have any missing expected elements, fail
+ if (!missing.isEmpty()) {
+ return failAtLeast(expected, missing);
+ }
+
+ /*
+ * TODO(cpovirk): In the NotInOrder case, also include a Fact that shows _only_ the required
+ * elements (that is, without any extras) but in the order they were actually found. That should
+ * make it easier for users to compare the actual order of the required elements to the expected
+ * order. Or, if that's too much trouble, at least try to find a better title for the full
+ * actual iterable than the default of "but was," which may _sound_ like it should show only the
+ * required elements, rather than the full actual iterable.
+ */
+ return ordered
+ ? IN_ORDER
+ : new Ordered() {
+ @Override
+ public void inOrder() {
+ failWithActual(
+ simpleFact("required elements were all found, but order was wrong"),
+ fact("expected order for required elements", expected));
+ }
+ };
+ }
+
+ /**
+ * Checks that the actual iterable contains at least all of the expected elements or fails. If an
+ * element appears more than once in the expected elements then it must appear at least that
+ * number of times in the actual elements.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method. The expected elements must appear in the given order
+ * within the actual elements, but they are not required to be consecutive.
+ */
+ @CanIgnoreReturnValue
+ public final Ordered containsAtLeastElementsIn(Object[] expected) {
+ return containsAtLeastElementsIn(asList(expected));
+ }
+
+ private Ordered failAtLeast(Collection<?> expected, Collection<?> missingRawObjects) {
+ Collection<?> nearMissRawObjects =
+ retainMatchingToString(actual, missingRawObjects /* itemsToCheck */);
+
+ ImmutableList.Builder<Fact> facts = ImmutableList.builder();
+ facts.addAll(
+ makeElementFactsForBoth(
+ "missing", missingRawObjects, "though it did contain", nearMissRawObjects));
+ /*
+ * TODO(cpovirk): Make makeElementFactsForBoth support generating just "though it did contain"
+ * rather than "though it did contain (2)?" Users might interpret the number as the *total*
+ * number of actual elements (or the total number of non-matched elements). (Frankly, they might
+ * think that even *without* the number.... Can we do better than the phrase "though it did
+ * contain," which has been our standard so far?) Or maybe it's all clear enough in context,
+ * since this error shows up only to inform users of type mismatches.
+ */
+ facts.add(fact("expected to contain at least", expected));
+ facts.add(butWas());
+
+ failWithoutActual(facts.build());
+ return ALREADY_FAILED;
+ }
+
+ /**
+ * Removes at most the given number of available elements from the input list and adds them to the
+ * given output collection.
+ */
+ private static void moveElements(List<?> input, Collection<Object> output, int maxElements) {
+ for (int i = 0; i < maxElements; i++) {
+ output.add(input.remove(0));
+ }
+ }
+
+ /**
+ * Checks that a subject contains exactly the provided objects or fails.
+ *
+ * <p>Multiplicity is respected. For example, an object duplicated exactly 3 times in the
+ * parameters asserts that the object must likewise be duplicated exactly 3 times in the subject.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method.
+ *
+ * <p>To test that the iterable contains the same elements as an array, prefer {@link
+ * #containsExactlyElementsIn(Object[])}. It makes clear that the given array is a list of
+ * elements, not an element itself. This helps human readers and avoids a compiler warning.
+ */
+ @CanIgnoreReturnValue
+ public final Ordered containsExactly(@Nullable Object /*@Nullable*/... varargs) {
+ List<Object> expected = (varargs == null) ? newArrayList((Object) null) : asList(varargs);
+ return containsExactlyElementsIn(
+ expected, varargs != null && varargs.length == 1 && varargs[0] instanceof Iterable);
+ }
+
+ /**
+ * Checks that a subject contains exactly the provided objects or fails.
+ *
+ * <p>Multiplicity is respected. For example, an object duplicated exactly 3 times in the {@code
+ * Iterable} parameter asserts that the object must likewise be duplicated exactly 3 times in the
+ * subject.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method.
+ */
+ @CanIgnoreReturnValue
+ public final Ordered containsExactlyElementsIn(Iterable<?> expected) {
+ return containsExactlyElementsIn(expected, false);
+ }
+
+ /**
+ * Checks that a subject contains exactly the provided objects or fails.
+ *
+ * <p>Multiplicity is respected. For example, an object duplicated exactly 3 times in the array
+ * parameter asserts that the object must likewise be duplicated exactly 3 times in the subject.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method.
+ */
+ @CanIgnoreReturnValue
+ public final Ordered containsExactlyElementsIn(Object[] expected) {
+ return containsExactlyElementsIn(asList(expected));
+ }
+
+ private Ordered containsExactlyElementsIn(
+ final Iterable<?> required, boolean addElementsInWarning) {
+ Iterator<?> actualIter = actual.iterator();
+ Iterator<?> requiredIter = required.iterator();
+
+ if (!requiredIter.hasNext()) {
+ if (actualIter.hasNext()) {
+ isEmpty(); // fails
+ return ALREADY_FAILED;
+ } else {
+ return IN_ORDER;
+ }
+ }
+
+ // Step through both iterators comparing elements pairwise.
+ boolean isFirst = true;
+ while (actualIter.hasNext() && requiredIter.hasNext()) {
+ Object actualElement = actualIter.next();
+ Object requiredElement = requiredIter.next();
+
+ // As soon as we encounter a pair of elements that differ, we know that inOrder()
+ // cannot succeed, so we can check the rest of the elements more normally.
+ // Since any previous pairs of elements we iterated over were equal, they have no
+ // effect on the result now.
+ if (!Objects.equal(actualElement, requiredElement)) {
+ if (isFirst && !actualIter.hasNext() && !requiredIter.hasNext()) {
+ /*
+ * There's exactly one actual element and exactly one expected element, and they don't
+ * match, so throw a ComparisonFailure. The logical way to do that would be
+ * `check(...).that(actualElement).isEqualTo(requiredElement)`. But isEqualTo has magic
+ * behavior for arrays and primitives, behavior that's inconsistent with how this method
+ * otherwise behaves. For consistency, we want to rely only on the equal() call we've
+ * already made. So we expose a special method for this and call it from here.
+ *
+ * TODO(b/135918662): Consider always throwing ComparisonFailure if there is exactly one
+ * missing and exactly one extra element, even if there were additional (matching)
+ * elements. However, this will probably be useful less often, and it will be tricky to
+ * explain. First, what would we say, "value of: iterable.onlyElementThatDidNotMatch()?"
+ * And second, it feels weirder to call out a single element when the expected and actual
+ * values had multiple elements. Granted, Fuzzy Truth already does this, so maybe it's OK?
+ * But Fuzzy Truth doesn't (yet) make the mismatched value so prominent.
+ */
+ checkNoNeedToDisplayBothValues("onlyElement()")
+ .that(actualElement)
+ .failEqualityCheckForEqualsWithoutDescription(requiredElement);
+ return ALREADY_FAILED;
+ }
+ // Missing elements; elements that are not missing will be removed as we iterate.
+ Collection<Object> missing = newArrayList();
+ missing.add(requiredElement);
+ Iterators.addAll(missing, requiredIter);
+
+ // Extra elements that the subject had but shouldn't have.
+ Collection<Object> extra = newArrayList();
+
+ // Remove all actual elements from missing, and add any that weren't in missing
+ // to extra.
+ if (!missing.remove(actualElement)) {
+ extra.add(actualElement);
+ }
+ while (actualIter.hasNext()) {
+ Object item = actualIter.next();
+ if (!missing.remove(item)) {
+ extra.add(item);
+ }
+ }
+
+ if (missing.isEmpty() && extra.isEmpty()) {
+ /*
+ * This containsExactly() call is a success. But the iterables were not in the same order,
+ * so return an object that will fail the test if the user calls inOrder().
+ */
+ return new Ordered() {
+ @Override
+ public void inOrder() {
+ failWithActual(
+ simpleFact("contents match, but order was wrong"), fact("expected", required));
+ }
+ };
+ }
+ return failExactly(required, addElementsInWarning, missing, extra);
+ }
+
+ isFirst = false;
+ }
+
+ // Here, we must have reached the end of one of the iterators without finding any
+ // pairs of elements that differ. If the actual iterator still has elements, they're
+ // extras. If the required iterator has elements, they're missing elements.
+ if (actualIter.hasNext()) {
+ return failExactly(
+ required,
+ addElementsInWarning,
+ /* missingRawObjects= */ ImmutableList.of(),
+ /* extraRawObjects= */ newArrayList(actualIter));
+ } else if (requiredIter.hasNext()) {
+ return failExactly(
+ required,
+ addElementsInWarning,
+ /* missingRawObjects= */ newArrayList(requiredIter),
+ /* extraRawObjects= */ ImmutableList.of());
+ }
+
+ // If neither iterator has elements, we reached the end and the elements were in
+ // order, so inOrder() can just succeed.
+ return IN_ORDER;
+ }
+
+ private Ordered failExactly(
+ Iterable<?> required,
+ boolean addElementsInWarning,
+ Collection<?> missingRawObjects,
+ Collection<?> extraRawObjects) {
+ ImmutableList.Builder<Fact> facts = ImmutableList.builder();
+ facts.addAll(
+ makeElementFactsForBoth("missing", missingRawObjects, "unexpected", extraRawObjects));
+ facts.add(fact("expected", required));
+ facts.add(butWas());
+ if (addElementsInWarning) {
+ facts.add(
+ simpleFact(
+ "Passing an iterable to the varargs method containsExactly(Object...) is "
+ + "often not the correct thing to do. Did you mean to call "
+ + "containsExactlyElementsIn(Iterable) instead?"));
+ }
+
+ failWithoutActual(facts.build());
+ return ALREADY_FAILED;
+ }
+
+ private static ImmutableList<Fact> makeElementFactsForBoth(
+ String firstKey,
+ Collection<?> firstCollection,
+ String secondKey,
+ Collection<?> secondCollection) {
+ // TODO(kak): Possible enhancement: Include "[1 copy]" if the element does appear in
+ // the subject but not enough times. Similarly for unexpected extra items.
+ boolean addTypeInfo = hasMatchingToStringPair(firstCollection, secondCollection);
+ DuplicateGroupedAndTyped first =
+ countDuplicatesAndMaybeAddTypeInfoReturnObject(firstCollection, addTypeInfo);
+ DuplicateGroupedAndTyped second =
+ countDuplicatesAndMaybeAddTypeInfoReturnObject(secondCollection, addTypeInfo);
+ ElementFactGrouping grouping = pickGrouping(first.entrySet(), second.entrySet());
+
+ ImmutableList.Builder<Fact> facts = ImmutableList.builder();
+ ImmutableList<Fact> firstFacts = makeElementFacts(firstKey, first, grouping);
+ ImmutableList<Fact> secondFacts = makeElementFacts(secondKey, second, grouping);
+ facts.addAll(firstFacts);
+ if (firstFacts.size() > 1 && secondFacts.size() > 1) {
+ facts.add(simpleFact(""));
+ }
+ facts.addAll(secondFacts);
+ facts.add(simpleFact("---"));
+ return facts.build();
+ }
+
+ /**
+ * Returns a list of facts (zero, one, or many, depending on the number of elements and the
+ * grouping policy) describing the given missing, unexpected, or near-miss elements.
+ */
+ private static ImmutableList<Fact> makeElementFacts(
+ String label, DuplicateGroupedAndTyped elements, ElementFactGrouping grouping) {
+ if (elements.isEmpty()) {
+ return ImmutableList.of();
+ }
+
+ if (grouping == ALL_IN_ONE_FACT) {
+ return ImmutableList.of(fact(keyToGoWithElementsString(label, elements), elements));
+ }
+
+ ImmutableList.Builder<Fact> facts = ImmutableList.builder();
+ facts.add(simpleFact(keyToServeAsHeader(label, elements)));
+ int n = 1;
+ for (Multiset.Entry<?> entry : elements.entrySet()) {
+ int count = entry.getCount();
+ Object item = entry.getElement();
+ facts.add(fact(numberString(n, count), item));
+ n += count;
+ }
+ return facts.build();
+ }
+
+ /*
+ * Fact keys like "missing (1)" go against our recommendation that keys should be fixed strings.
+ * But this violation lets the fact value contain only the elements (instead of also containing
+ * the count), so it feels worthwhile.
+ */
+
+ private static String keyToGoWithElementsString(String label, DuplicateGroupedAndTyped elements) {
+ /*
+ * elements.toString(), which the caller is going to use, includes the homogeneous type (if
+ * any), so we don't want to include it here. (And it's better to have it in the value, rather
+ * than in the key, so that it doesn't push the horizontally aligned values over too far.)
+ */
+ return lenientFormat("%s (%s)", label, elements.totalCopies());
+ }
+
+ private static String keyToServeAsHeader(String label, DuplicateGroupedAndTyped elements) {
+ /*
+ * The caller of this method outputs each individual element manually (as opposed to calling
+ * elements.toString()), so the homogeneous type isn't present unless we add it. Fortunately, we
+ * can add it here without pushing the horizontally aligned values over, as this key won't have
+ * an associated value, so it won't factor into alignment.
+ */
+ String key = keyToGoWithElementsString(label, elements);
+ if (elements.homogeneousTypeToDisplay.isPresent()) {
+ key += " (" + elements.homogeneousTypeToDisplay.get() + ")";
+ }
+ return key;
+ }
+
+ private static String numberString(int n, int count) {
+ return count == 1 ? lenientFormat("#%s", n) : lenientFormat("#%s [%s copies]", n, count);
+ }
+
+ private static ElementFactGrouping pickGrouping(
+ Iterable<Multiset.Entry<?>> first, Iterable<Multiset.Entry<?>> second) {
+ boolean firstHasMultiple = hasMultiple(first);
+ boolean secondHasMultiple = hasMultiple(second);
+ if ((firstHasMultiple || secondHasMultiple) && anyContainsCommaOrNewline(first, second)) {
+ return FACT_PER_ELEMENT;
+ }
+ if (firstHasMultiple && containsEmptyOrLong(first)) {
+ return FACT_PER_ELEMENT;
+ }
+ if (secondHasMultiple && containsEmptyOrLong(second)) {
+ return FACT_PER_ELEMENT;
+ }
+ return ALL_IN_ONE_FACT;
+ }
+
+ private static boolean anyContainsCommaOrNewline(Iterable<Multiset.Entry<?>>... lists) {
+ for (Multiset.Entry<?> entry : concat(lists)) {
+ String s = String.valueOf(entry.getElement());
+ if (s.contains("\n") || s.contains(",")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean hasMultiple(Iterable<Multiset.Entry<?>> entries) {
+ int totalCount = 0;
+ for (Multiset.Entry<?> entry : entries) {
+ totalCount += entry.getCount();
+ if (totalCount > 1) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean containsEmptyOrLong(Iterable<Multiset.Entry<?>> entries) {
+ int totalLength = 0;
+ for (Multiset.Entry<?> entry : entries) {
+ String s = entryString(entry);
+ if (s.isEmpty()) {
+ return true;
+ }
+ totalLength += s.length();
+ }
+ return totalLength > 200;
+ }
+
+ /**
+ * Whether to output each missing/unexpected item as its own {@link Fact} or to group all those
+ * items together into a single {@code Fact}.
+ */
+ enum ElementFactGrouping {
+ ALL_IN_ONE_FACT,
+ FACT_PER_ELEMENT;
+ }
+
+ /**
+ * Checks that a actual iterable contains none of the excluded objects or fails. (Duplicates are
+ * irrelevant to this test, which fails if any of the actual elements equal any of the excluded.)
+ */
+ public final void containsNoneOf(
+ @Nullable Object firstExcluded,
+ @Nullable Object secondExcluded,
+ @Nullable Object /*@Nullable*/... restOfExcluded) {
+ containsNoneIn(accumulate(firstExcluded, secondExcluded, restOfExcluded));
+ }
+
+ /**
+ * Checks that the actual iterable contains none of the elements contained in the excluded
+ * iterable or fails. (Duplicates are irrelevant to this test, which fails if any of the actual
+ * elements equal any of the excluded.)
+ */
+ public final void containsNoneIn(Iterable<?> excluded) {
+ Collection<?> actual = iterableToCollection(this.actual);
+ Collection<Object> present = new ArrayList<>();
+ for (Object item : Sets.newLinkedHashSet(excluded)) {
+ if (actual.contains(item)) {
+ present.add(item);
+ }
+ }
+ if (!present.isEmpty()) {
+ failWithoutActual(
+ fact("expected not to contain any of", annotateEmptyStrings(excluded)),
+ fact("but contained", annotateEmptyStrings(present)),
+ fullContents());
+ }
+ }
+
+ /**
+ * Checks that the actual iterable contains none of the elements contained in the excluded array
+ * or fails. (Duplicates are irrelevant to this test, which fails if any of the actual elements
+ * equal any of the excluded.)
+ */
+ public final void containsNoneIn(Object[] excluded) {
+ containsNoneIn(asList(excluded));
+ }
+
+ /** Ordered implementation that does nothing because it's already known to be true. */
+ @SuppressWarnings("UnnecessaryAnonymousClass") // for Java 7 compatibility
+ private static final Ordered IN_ORDER =
+ new Ordered() {
+ @Override
+ public void inOrder() {}
+ };
+
+ /** Ordered implementation that does nothing because an earlier check already caused a failure. */
+ @SuppressWarnings("UnnecessaryAnonymousClass") // for Java 7 compatibility
+ private static final Ordered ALREADY_FAILED =
+ new Ordered() {
+ @Override
+ public void inOrder() {}
+ };
+
+ /**
+ * Fails if the iterable is not strictly ordered, according to the natural ordering of its
+ * elements. Strictly ordered means that each element in the iterable is <i>strictly</i> greater
+ * than the element that preceded it.
+ *
+ * @throws ClassCastException if any pair of elements is not mutually Comparable
+ * @throws NullPointerException if any element is null
+ */
+ /*
+ * non-final because it's overridden by IterableOfProtosSubject.
+ *
+ * If we really, really wanted it to be final, we could make IterableOfProtosSubject implement a
+ * package-private(?) interface that redeclares this method as deprecated.
+ *
+ * Alternatively, we could avoid deprecating the method there, relying instead on Error Prone
+ * static analysis. It's _possible_ that users would be confused by having one overload deprecated
+ * and the other not, anyway.
+ */
+ public void isInStrictOrder() {
+ isInStrictOrder(Ordering.natural());
+ }
+
+ /**
+ * Fails if the iterable is not strictly ordered, according to the given comparator. Strictly
+ * ordered means that each element in the iterable is <i>strictly</i> greater than the element
+ * that preceded it.
+ *
+ * @throws ClassCastException if any pair of elements is not mutually Comparable
+ */
+ @SuppressWarnings({"unchecked"})
+ public final void isInStrictOrder(final Comparator<?> comparator) {
+ checkNotNull(comparator);
+ pairwiseCheck(
+ "expected to be in strict order",
+ new PairwiseChecker() {
+ @Override
+ public boolean check(Object prev, Object next) {
+ return ((Comparator<Object>) comparator).compare(prev, next) < 0;
+ }
+ });
+ }
+
+ /**
+ * Fails if the iterable is not ordered, according to the natural ordering of its elements.
+ * Ordered means that each element in the iterable is greater than or equal to the element that
+ * preceded it.
+ *
+ * @throws ClassCastException if any pair of elements is not mutually Comparable
+ * @throws NullPointerException if any element is null
+ */
+ // non-final because it's overridden by IterableOfProtosSubject. See isInStrictOrder.
+ public void isInOrder() {
+ isInOrder(Ordering.natural());
+ }
+
+ /**
+ * Fails if the iterable is not ordered, according to the given comparator. Ordered means that
+ * each element in the iterable is greater than or equal to the element that preceded it.
+ *
+ * @throws ClassCastException if any pair of elements is not mutually Comparable
+ */
+ @SuppressWarnings({"unchecked"})
+ public final void isInOrder(final Comparator<?> comparator) {
+ checkNotNull(comparator);
+ pairwiseCheck(
+ "expected to be in order",
+ new PairwiseChecker() {
+ @Override
+ public boolean check(Object prev, Object next) {
+ return ((Comparator<Object>) comparator).compare(prev, next) <= 0;
+ }
+ });
+ }
+
+ private interface PairwiseChecker {
+ boolean check(Object prev, Object next);
+ }
+
+ private void pairwiseCheck(String expectedFact, PairwiseChecker checker) {
+ Iterator<?> iterator = actual.iterator();
+ if (iterator.hasNext()) {
+ Object prev = iterator.next();
+ while (iterator.hasNext()) {
+ Object next = iterator.next();
+ if (!checker.check(prev, next)) {
+ failWithoutActual(
+ simpleFact(expectedFact),
+ fact("but contained", prev),
+ fact("followed by", next),
+ fullContents());
+ return;
+ }
+ prev = next;
+ }
+ }
+ }
+
+ /** @deprecated You probably meant to call {@link #containsNoneOf} instead. */
+ @Override
+ @Deprecated
+ public void isNoneOf(
+ @Nullable Object first, @Nullable Object second, @Nullable Object /*@Nullable*/... rest) {
+ super.isNoneOf(first, second, rest);
+ }
+
+ /** @deprecated You probably meant to call {@link #containsNoneIn} instead. */
+ @Override
+ @Deprecated
+ public void isNotIn(Iterable<?> iterable) {
+ if (Iterables.contains(iterable, actual)) {
+ failWithActual("expected not to be any of", iterable);
+ }
+ List<Object> nonIterables = new ArrayList<>();
+ for (Object element : iterable) {
+ if (!(element instanceof Iterable<?>)) {
+ nonIterables.add(element);
+ }
+ }
+ if (!nonIterables.isEmpty()) {
+ failWithoutActual(
+ simpleFact(
+ lenientFormat(
+ "The actual value is an Iterable, and you've written a test that compares it to "
+ + "some objects that are not Iterables. Did you instead mean to check "
+ + "whether its *contents* match any of the *contents* of the given values? "
+ + "If so, call containsNoneOf(...)/containsNoneIn(...) instead. "
+ + "Non-iterables: %s",
+ nonIterables)));
+ }
+ }
+
+ private Fact fullContents() {
+ return fact("full contents", actualCustomStringRepresentationForPackageMembersToCall());
+ }
+
+ /**
+ * Starts a method chain for a check in which the actual elements (i.e. the elements of the {@link
+ * Iterable} under test) are compared to expected elements using the given {@link Correspondence}.
+ * The actual elements must be of type {@code A}, the expected elements must be of type {@code E}.
+ * The check is actually executed by continuing the method chain. For example:
+ *
+ * <pre>{@code
+ * assertThat(actualIterable).comparingElementsUsing(correspondence).contains(expected);
+ * }</pre>
+ *
+ * where {@code actualIterable} is an {@code Iterable<A>} (or, more generally, an {@code
+ * Iterable<? extends A>}), {@code correspondence} is a {@code Correspondence<A, E>}, and {@code
+ * expected} is an {@code E}.
+ *
+ * <p>Any of the methods on the returned object may throw {@link ClassCastException} if they
+ * encounter an actual element that is not of type {@code A}.
+ */
+ public <A, E> UsingCorrespondence<A, E> comparingElementsUsing(
+ Correspondence<? super A, ? super E> correspondence) {
+ return new UsingCorrespondence<>(this, correspondence);
+ }
+
+ /**
+ * Starts a method chain for a check in which failure messages may use the given {@link
+ * DiffFormatter} to describe the difference between an actual elements (i.e. an element of the
+ * {@link Iterable} under test) and the element it is expected to be equal to, but isn't. The
+ * actual and expected elements must be of type {@code T}. The check is actually executed by
+ * continuing the method chain. You may well want to use {@link
+ * UsingCorrespondence#displayingDiffsPairedBy} to specify how the elements should be paired up
+ * for diffing. For example:
+ *
+ * <pre>{@code
+ * assertThat(actualFoos)
+ * .formattingDiffsUsing(FooTestHelper::formatDiff)
+ * .displayingDiffsPairedBy(Foo::getId)
+ * .containsExactly(foo1, foo2, foo3);
+ * }</pre>
+ *
+ * where {@code actualFoos} is an {@code Iterable<Foo>}, {@code FooTestHelper.formatDiff} is a
+ * static method taking two {@code Foo} arguments and returning a {@link String}, {@code
+ * Foo.getId} is a no-arg instance method returning some kind of ID, and {@code foo1}, {code
+ * foo2}, and {@code foo3} are {@code Foo} instances.
+ *
+ * <p>Unlike when using {@link #comparingElementsUsing}, the elements are still compared using
+ * object equality, so this method does not affect whether a test passes or fails.
+ *
+ * <p>Any of the methods on the returned object may throw {@link ClassCastException} if they
+ * encounter an actual element that is not of type {@code T}.
+ *
+ * @since 1.1
+ */
+ public <T> UsingCorrespondence<T, T> formattingDiffsUsing(
+ DiffFormatter<? super T, ? super T> formatter) {
+ return comparingElementsUsing(Correspondence.<T>equality().formattingDiffsUsing(formatter));
+ }
+
+ /**
+ * A partially specified check in which the actual elements (normally the elements of the {@link
+ * Iterable} under test) are compared to expected elements using a {@link Correspondence}. The
+ * expected elements are of type {@code E}. Call methods on this object to actually execute the
+ * check.
+ */
+ public static class UsingCorrespondence<A, E> {
+
+ private final IterableSubject subject;
+ private final Correspondence<? super A, ? super E> correspondence;
+ private final Optional<Pairer> pairer;
+
+ UsingCorrespondence(
+ IterableSubject subject, Correspondence<? super A, ? super E> correspondence) {
+ this.subject = checkNotNull(subject);
+ this.correspondence = checkNotNull(correspondence);
+ this.pairer = Optional.absent();
+ }
+
+ UsingCorrespondence(
+ IterableSubject subject,
+ Correspondence<? super A, ? super E> correspondence,
+ Pairer pairer) {
+ this.subject = checkNotNull(subject);
+ this.correspondence = checkNotNull(correspondence);
+ this.pairer = Optional.of(pairer);
+ }
+
+ /**
+ * Specifies a way to pair up unexpected and missing elements in the message when an assertion
+ * fails. For example:
+ *
+ * <pre>{@code
+ * assertThat(actualRecords)
+ * .comparingElementsUsing(RECORD_CORRESPONDENCE)
+ * .displayingDiffsPairedBy(Record::getId)
+ * .containsExactlyElementsIn(expectedRecords);
+ * }</pre>
+ *
+ * <p><b>Important</b>: The {code keyFunction} function must be able to accept both the actual
+ * and the unexpected elements, i.e. it must satisfy {@code Function<? super A, ?>} as well as
+ * {@code Function<? super E, ?>}. If that constraint is not met then a subsequent method may
+ * throw {@link ClassCastException}. Use the two-parameter overload if you need to specify
+ * different key functions for the actual and expected elements.
+ *
+ * <p>On assertions where it makes sense to do so, the elements are paired as follows: they are
+ * keyed by {@code keyFunction}, and if an unexpected element and a missing element have the
+ * same non-null key then the they are paired up. (Elements with null keys are not paired.) The
+ * failure message will show paired elements together, and a diff will be shown if the {@link
+ * Correspondence#formatDiff} method returns non-null.
+ *
+ * <p>The expected elements given in the assertion should be uniquely keyed by {@code
+ * keyFunction}. If multiple missing elements have the same key then the pairing will be
+ * skipped.
+ *
+ * <p>Useful key functions will have the property that key equality is less strict than the
+ * correspondence, i.e. given {@code actual} and {@code expected} values with keys {@code
+ * actualKey} and {@code expectedKey}, if {@code correspondence.compare(actual, expected)} is
+ * true then it is guaranteed that {@code actualKey} is equal to {@code expectedKey}, but there
+ * are cases where {@code actualKey} is equal to {@code expectedKey} but {@code
+ * correspondence.compare(actual, expected)} is false.
+ *
+ * <p>If the {@code apply} method on the key function throws an exception then the element will
+ * be treated as if it had a null key and not paired. (The first such exception will be noted in
+ * the failure message.)
+ *
+ * <p>Note that calling this method makes no difference to whether a test passes or fails, it
+ * just improves the message if it fails.
+ */
+ public UsingCorrespondence<A, E> displayingDiffsPairedBy(Function<? super E, ?> keyFunction) {
+ @SuppressWarnings("unchecked") // throwing ClassCastException is the correct behaviour
+ Function<? super A, ?> actualKeyFunction = (Function<? super A, ?>) keyFunction;
+ return displayingDiffsPairedBy(actualKeyFunction, keyFunction);
+ }
+
+ /**
+ * Specifies a way to pair up unexpected and missing elements in the message when an assertion
+ * fails. For example:
+ *
+ * <pre>{@code
+ * assertThat(actualFoos)
+ * .comparingElementsUsing(FOO_BAR_CORRESPONDENCE)
+ * .displayingDiffsPairedBy(Foo::getId, Bar::getFooId)
+ * .containsExactlyElementsIn(expectedBar);
+ * }</pre>
+ *
+ * <p>On assertions where it makes sense to do so, the elements are paired as follows: the
+ * unexpected elements are keyed by {@code actualKeyFunction}, the missing elements are keyed by
+ * {@code expectedKeyFunction}, and if an unexpected element and a missing element have the same
+ * non-null key then the they are paired up. (Elements with null keys are not paired.) The
+ * failure message will show paired elements together, and a diff will be shown if the {@link
+ * Correspondence#formatDiff} method returns non-null.
+ *
+ * <p>The expected elements given in the assertion should be uniquely keyed by {@code
+ * expectedKeyFunction}. If multiple missing elements have the same key then the pairing will be
+ * skipped.
+ *
+ * <p>Useful key functions will have the property that key equality is less strict than the
+ * correspondence, i.e. given {@code actual} and {@code expected} values with keys {@code
+ * actualKey} and {@code expectedKey}, if {@code correspondence.compare(actual, expected)} is
+ * true then it is guaranteed that {@code actualKey} is equal to {@code expectedKey}, but there
+ * are cases where {@code actualKey} is equal to {@code expectedKey} but {@code
+ * correspondence.compare(actual, expected)} is false.
+ *
+ * <p>If the {@code apply} method on either of the key functions throws an exception then the
+ * element will be treated as if it had a null key and not paired. (The first such exception
+ * will be noted in the failure message.)
+ *
+ * <p>Note that calling this method makes no difference to whether a test passes or fails, it
+ * just improves the message if it fails.
+ */
+ public UsingCorrespondence<A, E> displayingDiffsPairedBy(
+ Function<? super A, ?> actualKeyFunction, Function<? super E, ?> expectedKeyFunction) {
+ return new UsingCorrespondence<>(
+ subject, correspondence, new Pairer(actualKeyFunction, expectedKeyFunction));
+ }
+
+ /**
+ * Checks that the subject contains at least one element that corresponds to the given expected
+ * element.
+ */
+ public void contains(@Nullable E expected) {
+ Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forIterable();
+ for (A actual : getCastActual()) {
+ if (correspondence.safeCompare(actual, expected, exceptions)) {
+ // Found a match, but we still need to fail if we hit an exception along the way.
+ if (exceptions.hasCompareException()) {
+ subject.failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .addAll(exceptions.describeAsMainCause())
+ .add(fact("expected to contain", expected))
+ .addAll(correspondence.describeForIterable())
+ .add(fact("found match (but failing because of exception)", actual))
+ .add(subject.fullContents())
+ .build());
+ }
+ return;
+ }
+ }
+ // Found no match. Fail, reporting elements that have the correct key if there are any.
+ if (pairer.isPresent()) {
+ List<A> keyMatches = pairer.get().pairOne(expected, getCastActual(), exceptions);
+ if (!keyMatches.isEmpty()) {
+ subject.failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .add(fact("expected to contain", expected))
+ .addAll(correspondence.describeForIterable())
+ .add(simpleFact("but did not"))
+ .addAll(
+ formatExtras(
+ "though it did contain elements with correct key",
+ expected,
+ keyMatches,
+ exceptions))
+ .add(simpleFact("---"))
+ .add(subject.fullContents())
+ .addAll(exceptions.describeAsAdditionalInfo())
+ .build());
+ return;
+ }
+ }
+ subject.failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .add(fact("expected to contain", expected))
+ .addAll(correspondence.describeForIterable())
+ .add(subject.butWas())
+ .addAll(exceptions.describeAsAdditionalInfo())
+ .build());
+ }
+
+ /** Checks that none of the actual elements correspond to the given element. */
+ public void doesNotContain(@Nullable E excluded) {
+ Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forIterable();
+ List<A> matchingElements = new ArrayList<>();
+ for (A actual : getCastActual()) {
+ if (correspondence.safeCompare(actual, excluded, exceptions)) {
+ matchingElements.add(actual);
+ }
+ }
+ // Fail if we found any matches.
+ if (!matchingElements.isEmpty()) {
+ subject.failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .add(fact("expected not to contain", excluded))
+ .addAll(correspondence.describeForIterable())
+ .add(fact("but contained", countDuplicates(matchingElements)))
+ .add(subject.fullContents())
+ .addAll(exceptions.describeAsAdditionalInfo())
+ .build());
+ return;
+ }
+ // Found no match, but we still need to fail if we hit an exception along the way.
+ if (exceptions.hasCompareException()) {
+ subject.failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .addAll(exceptions.describeAsMainCause())
+ .add(fact("expected not to contain", excluded))
+ .addAll(correspondence.describeForIterable())
+ .add(simpleFact("found no match (but failing because of exception)"))
+ .add(subject.fullContents())
+ .build());
+ }
+ }
+
+ /**
+ * Checks that subject contains exactly elements that correspond to the expected elements, i.e.
+ * that there is a 1:1 mapping between the actual elements and the expected elements where each
+ * pair of elements correspond.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method.
+ *
+ * <p>To test that the iterable contains the elements corresponding to those in an array, prefer
+ * {@link #containsExactlyElementsIn(Object[])}. It makes clear that the given array is a list
+ * of elements, not an element itself. This helps human readers and avoids a compiler warning.
+ */
+ @SafeVarargs
+ @CanIgnoreReturnValue
+ public final Ordered containsExactly(@Nullable E /*@Nullable*/... expected) {
+ return containsExactlyElementsIn(
+ (expected == null) ? newArrayList((E) null) : asList(expected));
+ }
+
+ /**
+ * Checks that subject contains exactly elements that correspond to the expected elements, i.e.
+ * that there is a 1:1 mapping between the actual elements and the expected elements where each
+ * pair of elements correspond.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method.
+ */
+ @CanIgnoreReturnValue
+ public Ordered containsExactlyElementsIn(final Iterable<? extends E> expected) {
+ List<A> actualList = iterableToList(getCastActual());
+ List<? extends E> expectedList = iterableToList(expected);
+
+ if (expectedList.isEmpty()) {
+ if (actualList.isEmpty()) {
+ return IN_ORDER;
+ } else {
+ subject.isEmpty(); // fails
+ return ALREADY_FAILED;
+ }
+ }
+
+ // Check if the elements correspond in order. This allows the common case of a passing test
+ // using inOrder() to complete in linear time.
+ if (correspondInOrderExactly(actualList.iterator(), expectedList.iterator())) {
+ return IN_ORDER;
+ }
+
+ // We know they don't correspond in order, so we're going to have to do an any-order test.
+ // Find a many:many mapping between the indexes of the elements which correspond, and check
+ // it for completeness.
+ // Exceptions from Correspondence.compare are stored and treated as if false was returned.
+ Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forIterable();
+ ImmutableSetMultimap<Integer, Integer> candidateMapping =
+ findCandidateMapping(actualList, expectedList, exceptions);
+ if (failIfCandidateMappingHasMissingOrExtra(
+ actualList, expectedList, candidateMapping, exceptions)) {
+ return ALREADY_FAILED;
+ }
+ // We know that every expected element maps to at least one actual element, and vice versa.
+ // Find a maximal 1:1 mapping, and check it for completeness.
+ ImmutableBiMap<Integer, Integer> maximalOneToOneMapping =
+ findMaximalOneToOneMapping(candidateMapping);
+ if (failIfOneToOneMappingHasMissingOrExtra(
+ actualList, expectedList, maximalOneToOneMapping, exceptions)) {
+ return ALREADY_FAILED;
+ }
+ // Check whether we caught any exceptions from Correspondence.compare. We do the any-order
+ // assertions treating exceptions as if false was returned before this, because the failure
+ // messages are normally more useful (e.g. reporting that the actual iterable contained an
+ // unexpected null) but we are contractually obliged to throw here if the assertions passed.
+ if (exceptions.hasCompareException()) {
+ subject.failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .addAll(exceptions.describeAsMainCause())
+ .add(fact("expected", expected))
+ .addAll(correspondence.describeForIterable())
+ .add(simpleFact("found all expected elements (but failing because of exception)"))
+ .add(subject.fullContents())
+ .build());
+ return ALREADY_FAILED;
+ }
+ // The 1:1 mapping is complete, so the test succeeds (but we know from above that the mapping
+ // is not in order).
+ return new Ordered() {
+ @Override
+ public void inOrder() {
+ subject.failWithActual(
+ ImmutableList.<Fact>builder()
+ .add(simpleFact("contents match, but order was wrong"))
+ .add(fact("expected", expected))
+ .addAll(correspondence.describeForIterable())
+ .build());
+ }
+ };
+ }
+
+ /**
+ * Checks that subject contains exactly elements that correspond to the expected elements, i.e.
+ * that there is a 1:1 mapping between the actual elements and the expected elements where each
+ * pair of elements correspond.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method.
+ */
+ @CanIgnoreReturnValue
+ public Ordered containsExactlyElementsIn(E[] expected) {
+ return containsExactlyElementsIn(asList(expected));
+ }
+
+ /**
+ * Returns whether the actual and expected iterators have the same number of elements and, when
+ * iterated pairwise, every pair of actual and expected values satisfies the correspondence.
+ * Returns false if any comparison threw an exception.
+ */
+ private boolean correspondInOrderExactly(
+ Iterator<? extends A> actual, Iterator<? extends E> expected) {
+ Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forIterable();
+ while (actual.hasNext() && expected.hasNext()) {
+ A actualElement = actual.next();
+ E expectedElement = expected.next();
+ // Return false if the elements didn't correspond, or if the correspondence threw an
+ // exception. We'll fall back on the any-order assertion in this case.
+ if (!correspondence.safeCompare(actualElement, expectedElement, exceptions)) {
+ return false;
+ }
+ }
+ // No need to check the ExceptionStore, as we'll already have returned false on any exception.
+ return !(actual.hasNext() || expected.hasNext());
+ }
+
+ /**
+ * Given a list of actual elements and a list of expected elements, finds a many:many mapping
+ * between actual and expected elements where a pair of elements maps if it satisfies the
+ * correspondence. Returns this mapping as a multimap where the keys are indexes into the actual
+ * list and the values are indexes into the expected list. Any exceptions are treated as if the
+ * elements did not correspond, and the exception added to the store.
+ */
+ private ImmutableSetMultimap<Integer, Integer> findCandidateMapping(
+ List<? extends A> actual,
+ List<? extends E> expected,
+ Correspondence.ExceptionStore exceptions) {
+ ImmutableSetMultimap.Builder<Integer, Integer> mapping = ImmutableSetMultimap.builder();
+ for (int actualIndex = 0; actualIndex < actual.size(); actualIndex++) {
+ for (int expectedIndex = 0; expectedIndex < expected.size(); expectedIndex++) {
+ if (correspondence.safeCompare(
+ actual.get(actualIndex), expected.get(expectedIndex), exceptions)) {
+ mapping.put(actualIndex, expectedIndex);
+ }
+ }
+ }
+ return mapping.build();
+ }
+
+ /**
+ * Given a list of actual elements, a list of expected elements, and a many:many mapping between
+ * actual and expected elements specified as a multimap of indexes into the actual list to
+ * indexes into the expected list, checks that every actual element maps to at least one
+ * expected element and vice versa, and fails if this is not the case. Returns whether the
+ * assertion failed.
+ */
+ private boolean failIfCandidateMappingHasMissingOrExtra(
+ List<? extends A> actual,
+ List<? extends E> expected,
+ ImmutableSetMultimap<Integer, Integer> mapping,
+ Correspondence.ExceptionStore exceptions) {
+ List<? extends A> extra = findNotIndexed(actual, mapping.keySet());
+ List<? extends E> missing = findNotIndexed(expected, mapping.inverse().keySet());
+ if (!missing.isEmpty() || !extra.isEmpty()) {
+ subject.failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .addAll(describeMissingOrExtra(missing, extra, exceptions))
+ .add(fact("expected", expected))
+ .addAll(correspondence.describeForIterable())
+ .add(subject.butWas())
+ .addAll(exceptions.describeAsAdditionalInfo())
+ .build());
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Given a list of missing elements and a list of extra elements, at least one of which must be
+ * non-empty, returns facts describing them. Exceptions from calling {@link
+ * Correspondence#formatDiff} are stored in {@code exceptions}.
+ */
+ private ImmutableList<Fact> describeMissingOrExtra(
+ List<? extends E> missing,
+ List<? extends A> extra,
+ Correspondence.ExceptionStore exceptions) {
+ if (pairer.isPresent()) {
+ @Nullable Pairing pairing = pairer.get().pair(missing, extra, exceptions);
+ if (pairing != null) {
+ return describeMissingOrExtraWithPairing(pairing, exceptions);
+ } else {
+ return ImmutableList.<Fact>builder()
+ .addAll(describeMissingOrExtraWithoutPairing(missing, extra))
+ .add(
+ simpleFact(
+ "a key function which does not uniquely key the expected elements was"
+ + " provided and has consequently been ignored"))
+ .build();
+ }
+ } else if (missing.size() == 1 && extra.size() >= 1) {
+ return ImmutableList.<Fact>builder()
+ .add(fact("missing (1)", missing.get(0)))
+ .addAll(formatExtras("unexpected", missing.get(0), extra, exceptions))
+ .add(simpleFact("---"))
+ .build();
+ } else {
+ return describeMissingOrExtraWithoutPairing(missing, extra);
+ }
+ }
+
+ private ImmutableList<Fact> describeMissingOrExtraWithoutPairing(
+ List<? extends E> missing, List<? extends A> extra) {
+ return makeElementFactsForBoth("missing", missing, "unexpected", extra);
+ }
+
+ private ImmutableList<Fact> describeMissingOrExtraWithPairing(
+ Pairing pairing, Correspondence.ExceptionStore exceptions) {
+ ImmutableList.Builder<Fact> facts = ImmutableList.builder();
+ for (Object key : pairing.pairedKeysToExpectedValues.keySet()) {
+ E missing = pairing.pairedKeysToExpectedValues.get(key);
+ List<A> extras = pairing.pairedKeysToActualValues.get(key);
+ facts.add(fact("for key", key));
+ facts.add(fact("missing", missing));
+ facts.addAll(formatExtras("unexpected", missing, extras, exceptions));
+ facts.add(simpleFact("---"));
+ }
+ if (!pairing.unpairedActualValues.isEmpty() || !pairing.unpairedExpectedValues.isEmpty()) {
+ facts.add(simpleFact("elements without matching keys:"));
+ facts.addAll(
+ describeMissingOrExtraWithoutPairing(
+ pairing.unpairedExpectedValues, pairing.unpairedActualValues));
+ }
+ return facts.build();
+ }
+
+ private ImmutableList<Fact> formatExtras(
+ String label,
+ E missing,
+ List<? extends A> extras,
+ Correspondence.ExceptionStore exceptions) {
+ List<String> diffs = new ArrayList<>(extras.size());
+ boolean hasDiffs = false;
+ for (int i = 0; i < extras.size(); i++) {
+ A extra = extras.get(i);
+ @Nullable String diff = correspondence.safeFormatDiff(extra, missing, exceptions);
+ diffs.add(diff);
+ if (diff != null) {
+ hasDiffs = true;
+ }
+ }
+ if (hasDiffs) {
+ ImmutableList.Builder<Fact> extraFacts = ImmutableList.builder();
+ extraFacts.add(simpleFact(lenientFormat("%s (%s)", label, extras.size())));
+ for (int i = 0; i < extras.size(); i++) {
+ A extra = extras.get(i);
+ extraFacts.add(fact(lenientFormat("#%s", i + 1), extra));
+ if (diffs.get(i) != null) {
+ extraFacts.add(fact("diff", diffs.get(i)));
+ }
+ }
+ return extraFacts.build();
+ } else {
+ return ImmutableList.of(
+ fact(lenientFormat("%s (%s)", label, extras.size()), countDuplicates(extras)));
+ }
+ }
+
+ /**
+ * Returns all the elements of the given list other than those with the given indexes. Assumes
+ * that all the given indexes really are valid indexes into the list.
+ */
+ private <T> List<T> findNotIndexed(List<T> list, Set<Integer> indexes) {
+ if (indexes.size() == list.size()) {
+ // If there are as many distinct valid indexes are there are elements in the list then every
+ // index must be in there once.
+ return asList();
+ }
+ List<T> notIndexed = newArrayList();
+ for (int index = 0; index < list.size(); index++) {
+ if (!indexes.contains(index)) {
+ notIndexed.add(list.get(index));
+ }
+ }
+ return notIndexed;
+ }
+
+ /**
+ * Given a many:many mapping between actual elements and expected elements, finds a 1:1 mapping
+ * which is the subset of that many:many mapping which includes the largest possible number of
+ * elements. The input and output mappings are each described as a map or multimap where the
+ * keys are indexes into the actual list and the values are indexes into the expected list. If
+ * there are multiple possible output mappings tying for the largest possible, this returns an
+ * arbitrary one.
+ */
+ private ImmutableBiMap<Integer, Integer> findMaximalOneToOneMapping(
+ ImmutableMultimap<Integer, Integer> edges) {
+ /*
+ * Finding this 1:1 mapping is analogous to finding a maximum cardinality bipartite matching
+ * (https://en.wikipedia.org/wiki/Matching_(graph_theory)#In_unweighted_bipartite_graphs).
+ * - The two sets of elements together correspond to the vertices of a graph.
+ * - The many:many mapping corresponds to the edges of that graph.
+ * - The graph is therefore bipartite, with the two sets of elements corresponding to the two
+ * parts.
+ * - A 1:1 mapping corresponds to a matching on that bipartite graph (aka an independent edge
+ * set, i.e. a subset of the edges with no common vertices).
+ * - And the 1:1 mapping which includes the largest possible number of elements corresponds
+ * to the maximum cardinality matching.
+ *
+ * So we'll apply a standard algorithm for doing maximum cardinality bipartite matching.
+ */
+ return GraphMatching.maximumCardinalityBipartiteMatching(edges);
+ }
+
+ /**
+ * Given a list of actual elements, a list of expected elements, and a 1:1 mapping between
+ * actual and expected elements specified as a bimap of indexes into the actual list to indexes
+ * into the expected list, checks that every actual element maps to an expected element and vice
+ * versa, and fails if this is not the case. Returns whether the assertion failed.
+ */
+ private boolean failIfOneToOneMappingHasMissingOrExtra(
+ List<? extends A> actual,
+ List<? extends E> expected,
+ BiMap<Integer, Integer> mapping,
+ Correspondence.ExceptionStore exceptions) {
+ List<? extends A> extra = findNotIndexed(actual, mapping.keySet());
+ List<? extends E> missing = findNotIndexed(expected, mapping.values());
+ if (!missing.isEmpty() || !extra.isEmpty()) {
+ subject.failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .add(
+ simpleFact(
+ "in an assertion requiring a 1:1 mapping between the expected and the"
+ + " actual elements, each actual element matches as least one expected"
+ + " element, and vice versa, but there was no 1:1 mapping"))
+ .add(
+ simpleFact(
+ "using the most complete 1:1 mapping (or one such mapping, if there is a"
+ + " tie)"))
+ .addAll(describeMissingOrExtra(missing, extra, exceptions))
+ .add(fact("expected", expected))
+ .addAll(correspondence.describeForIterable())
+ .add(subject.butWas())
+ .addAll(exceptions.describeAsAdditionalInfo())
+ .build());
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Checks that the subject contains elements that corresponds to all of the expected elements,
+ * i.e. that there is a 1:1 mapping between any subset of the actual elements and the expected
+ * elements where each pair of elements correspond.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method. The elements must appear in the given order within the
+ * subject, but they are not required to be consecutive.
+ */
+ @SafeVarargs
+ @CanIgnoreReturnValue
+ public final Ordered containsAtLeast(
+ @Nullable E first, @Nullable E second, @Nullable E /*@Nullable*/... rest) {
+ return containsAtLeastElementsIn(accumulate(first, second, rest));
+ }
+
+ /**
+ * Checks that the subject contains elements that corresponds to all of the expected elements,
+ * i.e. that there is a 1:1 mapping between any subset of the actual elements and the expected
+ * elements where each pair of elements correspond.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method. The elements must appear in the given order within the
+ * subject, but they are not required to be consecutive.
+ */
+ @CanIgnoreReturnValue
+ public Ordered containsAtLeastElementsIn(final Iterable<? extends E> expected) {
+ List<A> actualList = iterableToList(getCastActual());
+ List<? extends E> expectedList = iterableToList(expected);
+ // Check if the expected elements correspond in order to any subset of the actual elements.
+ // This allows the common case of a passing test using inOrder() to complete in linear time.
+ if (correspondInOrderAllIn(actualList.iterator(), expectedList.iterator())) {
+ return IN_ORDER;
+ }
+ // We know they don't correspond in order, so we're going to have to do an any-order test.
+ // Find a many:many mapping between the indexes of the elements which correspond, and check
+ // it for completeness.
+ Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forIterable();
+ ImmutableSetMultimap<Integer, Integer> candidateMapping =
+ findCandidateMapping(actualList, expectedList, exceptions);
+ if (failIfCandidateMappingHasMissing(
+ actualList, expectedList, candidateMapping, exceptions)) {
+ return ALREADY_FAILED;
+ }
+ // We know that every expected element maps to at least one actual element, and vice versa.
+ // Find a maximal 1:1 mapping, and check it for completeness.
+ ImmutableBiMap<Integer, Integer> maximalOneToOneMapping =
+ findMaximalOneToOneMapping(candidateMapping);
+ if (failIfOneToOneMappingHasMissing(
+ actualList, expectedList, maximalOneToOneMapping, exceptions)) {
+ return ALREADY_FAILED;
+ }
+ // Check whether we caught any exceptions from Correspondence.compare. As with
+ // containsExactlyElementIn, we do the any-order assertions treating exceptions as if false
+ // was returned before this, but we are contractually obliged to throw here if the assertions
+ // passed.
+ if (exceptions.hasCompareException()) {
+ subject.failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .addAll(exceptions.describeAsMainCause())
+ .add(fact("expected to contain at least", expected))
+ .addAll(correspondence.describeForIterable())
+ .add(simpleFact("found all expected elements (but failing because of exception)"))
+ .add(subject.fullContents())
+ .build());
+ return ALREADY_FAILED;
+ }
+ // The 1:1 mapping maps all the expected elements, so the test succeeds (but we know from
+ // above that the mapping is not in order).
+ return new Ordered() {
+ @Override
+ public void inOrder() {
+ subject.failWithActual(
+ ImmutableList.<Fact>builder()
+ .add(simpleFact("required elements were all found, but order was wrong"))
+ .add(fact("expected order for required elements", expected))
+ .addAll(correspondence.describeForIterable())
+ .build());
+ }
+ };
+ }
+
+ /**
+ * Checks that the subject contains elements that corresponds to all of the expected elements,
+ * i.e. that there is a 1:1 mapping between any subset of the actual elements and the expected
+ * elements where each pair of elements correspond.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method. The elements must appear in the given order within the
+ * subject, but they are not required to be consecutive.
+ */
+ @CanIgnoreReturnValue
+ public Ordered containsAtLeastElementsIn(E[] expected) {
+ return containsAtLeastElementsIn(asList(expected));
+ }
+
+ /**
+ * Returns whether all the elements of the expected iterator and any subset of the elements of
+ * the actual iterator can be paired up in order, such that every pair of actual and expected
+ * elements satisfies the correspondence. Returns false if any comparison threw an exception.
+ */
+ private boolean correspondInOrderAllIn(
+ Iterator<? extends A> actual, Iterator<? extends E> expected) {
+ // We take a greedy approach here, iterating through the expected elements and pairing each
+ // with the first applicable actual element. This is fine for the in-order test, since there's
+ // no way that paring an expected element with a later actual element permits a solution which
+ // couldn't be achieved by pairing it with the first. (For the any-order test, we may want to
+ // pair an expected element with a later actual element so that we can pair the earlier actual
+ // element with a later expected element, but that doesn't apply here.)
+ Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forIterable();
+ while (expected.hasNext()) {
+ E expectedElement = expected.next();
+ // Return false if we couldn't find the expected exception, or if the correspondence threw
+ // an exception. We'll fall back on the any-order assertion in this case.
+ if (!findCorresponding(actual, expectedElement, exceptions)
+ || exceptions.hasCompareException()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Advances the actual iterator looking for an element which corresponds to the expected
+ * element. Returns whether or not it finds one.
+ */
+ private boolean findCorresponding(
+ Iterator<? extends A> actual, E expectedElement, Correspondence.ExceptionStore exceptions) {
+ while (actual.hasNext()) {
+ A actualElement = actual.next();
+ if (correspondence.safeCompare(actualElement, expectedElement, exceptions)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Given a list of actual elements, a list of expected elements, and a many:many mapping between
+ * actual and expected elements specified as a multimap of indexes into an actual list to
+ * indexes into the expected list, checks that every expected element maps to at least one
+ * actual element, and fails if this is not the case. Actual elements which do not map to any
+ * expected elements are ignored.
+ */
+ private boolean failIfCandidateMappingHasMissing(
+ List<? extends A> actual,
+ List<? extends E> expected,
+ ImmutableSetMultimap<Integer, Integer> mapping,
+ Correspondence.ExceptionStore exceptions) {
+ List<? extends E> missing = findNotIndexed(expected, mapping.inverse().keySet());
+ if (!missing.isEmpty()) {
+ List<? extends A> extra = findNotIndexed(actual, mapping.keySet());
+ subject.failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .addAll(describeMissing(missing, extra, exceptions))
+ .add(fact("expected to contain at least", expected))
+ .addAll(correspondence.describeForIterable())
+ .add(subject.butWas())
+ .addAll(exceptions.describeAsAdditionalInfo())
+ .build());
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Given a list of missing elements, which must be non-empty, and a list of extra elements,
+ * returns a list of facts describing the missing elements, diffing against the extra ones where
+ * appropriate.
+ */
+ private ImmutableList<Fact> describeMissing(
+ List<? extends E> missing,
+ List<? extends A> extra,
+ Correspondence.ExceptionStore exceptions) {
+ if (pairer.isPresent()) {
+ @Nullable Pairing pairing = pairer.get().pair(missing, extra, exceptions);
+ if (pairing != null) {
+ return describeMissingWithPairing(pairing, exceptions);
+ } else {
+ return ImmutableList.<Fact>builder()
+ .addAll(describeMissingWithoutPairing(missing))
+ .add(
+ simpleFact(
+ "a key function which does not uniquely key the expected elements was"
+ + " provided and has consequently been ignored"))
+ .build();
+ }
+ } else {
+ // N.B. For containsAny, we do not treat having exactly one missing element as a special
+ // case (as we do for containsExactly). Showing extra elements has lower utility for
+ // containsAny (because they are allowed by the assertion) so we only show them if the user
+ // has explicitly opted in by specifying a pairing.
+ return describeMissingWithoutPairing(missing);
+ }
+ }
+
+ private ImmutableList<Fact> describeMissingWithoutPairing(List<? extends E> missing) {
+ return makeElementFactsForBoth("missing", missing, "unexpected", ImmutableList.of());
+ }
+
+ private ImmutableList<Fact> describeMissingWithPairing(
+ Pairing pairing, Correspondence.ExceptionStore exceptions) {
+ ImmutableList.Builder<Fact> facts = ImmutableList.builder();
+ for (Object key : pairing.pairedKeysToExpectedValues.keySet()) {
+ E missing = pairing.pairedKeysToExpectedValues.get(key);
+ List<A> extras = pairing.pairedKeysToActualValues.get(key);
+ facts.add(fact("for key", key));
+ facts.add(fact("missing", missing));
+ facts.addAll(
+ formatExtras("did contain elements with that key", missing, extras, exceptions));
+ facts.add(simpleFact("---"));
+ }
+ if (!pairing.unpairedExpectedValues.isEmpty()) {
+ facts.add(simpleFact("elements without matching keys:"));
+ facts.addAll(describeMissingWithoutPairing(pairing.unpairedExpectedValues));
+ }
+ return facts.build();
+ }
+
+ /**
+ * Given a list of expected elements, and a 1:1 mapping between actual and expected elements
+ * specified as a bimap of indexes into an actual list to indexes into the expected list, checks
+ * that every expected element maps to an actual element. Actual elements which do not map to
+ * any expected elements are ignored.
+ */
+ private boolean failIfOneToOneMappingHasMissing(
+ List<? extends A> actual,
+ List<? extends E> expected,
+ BiMap<Integer, Integer> mapping,
+ Correspondence.ExceptionStore exceptions) {
+ List<? extends E> missing = findNotIndexed(expected, mapping.values());
+ if (!missing.isEmpty()) {
+ List<? extends A> extra = findNotIndexed(actual, mapping.keySet());
+ subject.failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .add(
+ simpleFact(
+ "in an assertion requiring a 1:1 mapping between the expected and a subset"
+ + " of the actual elements, each actual element matches as least one"
+ + " expected element, and vice versa, but there was no 1:1 mapping"))
+ .add(
+ simpleFact(
+ "using the most complete 1:1 mapping (or one such mapping, if there is a"
+ + " tie)"))
+ .addAll(describeMissing(missing, extra, exceptions))
+ .add(fact("expected to contain at least", expected))
+ .addAll(correspondence.describeForIterable())
+ .add(subject.butWas())
+ .addAll(exceptions.describeAsAdditionalInfo())
+ .build());
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Checks that the subject contains at least one element that corresponds to at least one of the
+ * expected elements.
+ */
+ @SafeVarargs
+ public final void containsAnyOf(
+ @Nullable E first, @Nullable E second, @Nullable E /*@Nullable*/... rest) {
+ containsAnyIn(accumulate(first, second, rest));
+ }
+
+ /**
+ * Checks that the subject contains at least one element that corresponds to at least one of the
+ * expected elements.
+ */
+ public void containsAnyIn(Iterable<? extends E> expected) {
+ Collection<A> actual = iterableToCollection(getCastActual());
+ Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forIterable();
+ for (E expectedItem : expected) {
+ for (A actualItem : actual) {
+ if (correspondence.safeCompare(actualItem, expectedItem, exceptions)) {
+ // Found a match, but we still need to fail if we hit an exception along the way.
+ if (exceptions.hasCompareException()) {
+ subject.failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .addAll(exceptions.describeAsMainCause())
+ .add(fact("expected to contain any of", expected))
+ .addAll(correspondence.describeForIterable())
+ .add(simpleFact("found match (but failing because of exception)"))
+ .add(subject.fullContents())
+ .build());
+ }
+ return;
+ }
+ }
+ }
+ // Found no match. Fail, reporting elements that have a correct key if there are any.
+ if (pairer.isPresent()) {
+ Pairing pairing =
+ pairer.get().pair(iterableToList(expected), iterableToList(actual), exceptions);
+ if (pairing != null) {
+ if (!pairing.pairedKeysToExpectedValues.isEmpty()) {
+ subject.failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .add(fact("expected to contain any of", expected))
+ .addAll(correspondence.describeForIterable())
+ .add(subject.butWas())
+ .addAll(describeAnyMatchesByKey(pairing, exceptions))
+ .addAll(exceptions.describeAsAdditionalInfo())
+ .build());
+ } else {
+ subject.failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .add(fact("expected to contain any of", expected))
+ .addAll(correspondence.describeForIterable())
+ .add(subject.butWas())
+ .add(simpleFact("it does not contain any matches by key, either"))
+ .addAll(exceptions.describeAsAdditionalInfo())
+ .build());
+ }
+ } else {
+ subject.failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .add(fact("expected to contain any of", expected))
+ .addAll(correspondence.describeForIterable())
+ .add(subject.butWas())
+ .add(
+ simpleFact(
+ "a key function which does not uniquely key the expected elements was"
+ + " provided and has consequently been ignored"))
+ .addAll(exceptions.describeAsAdditionalInfo())
+ .build());
+ }
+ } else {
+ subject.failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .add(fact("expected to contain any of", expected))
+ .addAll(correspondence.describeForIterable())
+ .add(subject.butWas())
+ .addAll(exceptions.describeAsAdditionalInfo())
+ .build());
+ }
+ }
+
+ /**
+ * Checks that the subject contains at least one element that corresponds to at least one of the
+ * expected elements.
+ */
+ public void containsAnyIn(E[] expected) {
+ containsAnyIn(asList(expected));
+ }
+
+ private ImmutableList<Fact> describeAnyMatchesByKey(
+ Pairing pairing, Correspondence.ExceptionStore exceptions) {
+ ImmutableList.Builder<Fact> facts = ImmutableList.builder();
+ for (Object key : pairing.pairedKeysToExpectedValues.keySet()) {
+ E expected = pairing.pairedKeysToExpectedValues.get(key);
+ List<A> got = pairing.pairedKeysToActualValues.get(key);
+ facts.add(fact("for key", key));
+ facts.add(fact("expected any of", expected));
+ facts.addAll(formatExtras("but got", expected, got, exceptions));
+ facts.add(simpleFact("---"));
+ }
+ return facts.build();
+ }
+
+ /**
+ * Checks that the subject contains no elements that correspond to any of the given elements.
+ * (Duplicates are irrelevant to this test, which fails if any of the subject elements
+ * correspond to any of the given elements.)
+ */
+ @SafeVarargs
+ public final void containsNoneOf(
+ @Nullable E firstExcluded,
+ @Nullable E secondExcluded,
+ @Nullable E /*@Nullable*/... restOfExcluded) {
+ containsNoneIn(accumulate(firstExcluded, secondExcluded, restOfExcluded));
+ }
+
+ /**
+ * Checks that the subject contains no elements that correspond to any of the given elements.
+ * (Duplicates are irrelevant to this test, which fails if any of the subject elements
+ * correspond to any of the given elements.)
+ */
+ public void containsNoneIn(Iterable<? extends E> excluded) {
+ Collection<A> actual = iterableToCollection(getCastActual());
+ ListMultimap<E, A> present = LinkedListMultimap.create();
+ Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forIterable();
+ for (E excludedItem : Sets.newLinkedHashSet(excluded)) {
+ for (A actualItem : actual) {
+ if (correspondence.safeCompare(actualItem, excludedItem, exceptions)) {
+ present.put(excludedItem, actualItem);
+ }
+ }
+ }
+ // Fail if we found any matches.
+ if (!present.isEmpty()) {
+ ImmutableList.Builder<Fact> facts = ImmutableList.builder();
+ facts.add(fact("expected not to contain any of", annotateEmptyStrings(excluded)));
+ facts.addAll(correspondence.describeForIterable());
+ for (E excludedItem : present.keySet()) {
+ List<A> actualItems = present.get(excludedItem);
+ facts.add(fact("but contained", annotateEmptyStrings(actualItems)));
+ facts.add(fact("corresponding to", excludedItem));
+ facts.add(simpleFact("---"));
+ }
+ facts.add(subject.fullContents());
+ facts.addAll(exceptions.describeAsAdditionalInfo());
+ subject.failWithoutActual(facts.build());
+
+ return;
+ }
+ // Found no match, but we still need to fail if we hit an exception along the way.
+ if (exceptions.hasCompareException()) {
+ subject.failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .addAll(exceptions.describeAsMainCause())
+ .add(fact("expected not to contain any of", annotateEmptyStrings(excluded)))
+ .addAll(correspondence.describeForIterable())
+ .add(simpleFact("found no matches (but failing because of exception)"))
+ .add(subject.fullContents())
+ .build());
+ }
+ }
+
+ /**
+ * Checks that the subject contains no elements that correspond to any of the given elements.
+ * (Duplicates are irrelevant to this test, which fails if any of the subject elements
+ * correspond to any of the given elements.)
+ */
+ public void containsNoneIn(E[] excluded) {
+ containsNoneIn(asList(excluded));
+ }
+
+ @SuppressWarnings("unchecked") // throwing ClassCastException is the correct behaviour
+ private Iterable<A> getCastActual() {
+ return (Iterable<A>) subject.actual;
+ }
+
+ // TODO(b/69154276): Consider commoning up some of the logic between IterableSubject.Pairer,
+ // MapSubject.MapDifference, and MultimapSubject.difference(). We are likely to need something
+ // similar again when we do the work to improve the failure messages from
+ // MultimapSubject.UsingCorrespondence (because it won't be able to delegate to
+ // IterableSubject.UsingCorrespondence like it does now). So it makes sense to do the
+ // refactoring as part of that. Right now, we don't even know what Multimap is going to need.
+
+ /**
+ * A class which knows how to pair the actual and expected elements (see {@link
+ * #displayingDiffsPairedBy}).
+ */
+ private final class Pairer {
+
+ private final Function<? super A, ?> actualKeyFunction;
+ private final Function<? super E, ?> expectedKeyFunction;
+
+ Pairer(Function<? super A, ?> actualKeyFunction, Function<? super E, ?> expectedKeyFunction) {
+ this.actualKeyFunction = actualKeyFunction;
+ this.expectedKeyFunction = expectedKeyFunction;
+ }
+
+ /**
+ * Returns a {@link Pairing} of the given expected and actual values, or {@code null} if the
+ * expected values are not uniquely keyed.
+ */
+ @Nullable
+ Pairing pair(
+ List<? extends E> expectedValues,
+ List<? extends A> actualValues,
+ Correspondence.ExceptionStore exceptions) {
+ Pairing pairing = new Pairing();
+
+ // Populate expectedKeys with the keys of the corresponding elements of expectedValues.
+ // We do this ahead of time to avoid invoking the key function twice for each element.
+ List<Object> expectedKeys = new ArrayList<>(expectedValues.size());
+ for (E expected : expectedValues) {
+ expectedKeys.add(expectedKey(expected, exceptions));
+ }
+
+ // Populate pairedKeysToExpectedValues with *all* the expected values with non-null keys.
+ // We will remove the unpaired keys later. Return null if we find a duplicate key.
+ for (int i = 0; i < expectedValues.size(); i++) {
+ E expected = expectedValues.get(i);
+ @Nullable Object key = expectedKeys.get(i);
+ if (key != null) {
+ if (pairing.pairedKeysToExpectedValues.containsKey(key)) {
+ return null;
+ } else {
+ pairing.pairedKeysToExpectedValues.put(key, expected);
+ }
+ }
+ }
+
+ // Populate pairedKeysToActualValues and unpairedActualValues.
+ for (A actual : actualValues) {
+ @Nullable Object key = actualKey(actual, exceptions);
+ if (pairing.pairedKeysToExpectedValues.containsKey(key)) {
+ pairing.pairedKeysToActualValues.put(key, actual);
+ } else {
+ pairing.unpairedActualValues.add(actual);
+ }
+ }
+
+ // Populate unpairedExpectedValues and remove unpaired keys from pairedKeysToExpectedValues.
+ for (int i = 0; i < expectedValues.size(); i++) {
+ E expected = expectedValues.get(i);
+ @Nullable Object key = expectedKeys.get(i);
+ if (!pairing.pairedKeysToActualValues.containsKey(key)) {
+ pairing.unpairedExpectedValues.add(expected);
+ pairing.pairedKeysToExpectedValues.remove(key);
+ }
+ }
+
+ return pairing;
+ }
+
+ List<A> pairOne(
+ E expectedValue,
+ Iterable<? extends A> actualValues,
+ Correspondence.ExceptionStore exceptions) {
+ @Nullable Object key = expectedKey(expectedValue, exceptions);
+ List<A> matches = new ArrayList<>();
+ if (key != null) {
+ for (A actual : actualValues) {
+ if (key.equals(actualKey(actual, exceptions))) {
+ matches.add(actual);
+ }
+ }
+ }
+ return matches;
+ }
+
+ private @Nullable Object actualKey(A actual, Correspondence.ExceptionStore exceptions) {
+ try {
+ return actualKeyFunction.apply(actual);
+ } catch (RuntimeException e) {
+ exceptions.addActualKeyFunctionException(
+ IterableSubject.UsingCorrespondence.Pairer.class, e, actual);
+ return null;
+ }
+ }
+
+ private @Nullable Object expectedKey(E expected, Correspondence.ExceptionStore exceptions) {
+ try {
+ return expectedKeyFunction.apply(expected);
+ } catch (RuntimeException e) {
+ exceptions.addExpectedKeyFunctionException(
+ IterableSubject.UsingCorrespondence.Pairer.class, e, expected);
+ return null;
+ }
+ }
+ }
+
+ /** An description of a pairing between expected and actual values. N.B. This is mutable. */
+ private final class Pairing {
+
+ /**
+ * Map from keys used in the pairing to the expected value with that key. Iterates in the
+ * order the expected values appear in the input. Will never contain null keys.
+ */
+ private final Map<Object, E> pairedKeysToExpectedValues = new LinkedHashMap<>();
+
+ /**
+ * Multimap from keys used in the pairing to the actual values with that key. Keys iterate in
+ * the order they first appear in the actual values in the input, and values for each key
+ * iterate in the order they appear too. Will never contain null keys.
+ */
+ private final ListMultimap<Object, A> pairedKeysToActualValues = LinkedListMultimap.create();
+
+ /**
+ * List of the expected values not used in the pairing. Iterates in the order they appear in
+ * the input.
+ */
+ private final List<E> unpairedExpectedValues = newArrayList();
+
+ /**
+ * List of the actual values not used in the pairing. Iterates in the order they appear in the
+ * input.
+ */
+ private final List<A> unpairedActualValues = newArrayList();
+ }
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/LazyMessage.java b/core/src/main/java/com/google/common/truth/LazyMessage.java
new file mode 100644
index 00000000..aa5b3c54
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/LazyMessage.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Strings.lenientFormat;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+
+final class LazyMessage {
+ private static final String PLACEHOLDER_ERR =
+ "Incorrect number of args (%s) for the given placeholders (%s) in string template:\"%s\"";
+
+ private final String format;
+ private final Object[] args;
+
+ LazyMessage(String format, /*@Nullable*/ Object... args) {
+ this.format = format;
+ this.args = args;
+ int placeholders = countPlaceholders(format);
+ checkArgument(placeholders == args.length, PLACEHOLDER_ERR, args.length, placeholders, format);
+ }
+
+ @Override
+ public String toString() {
+ return lenientFormat(format, args);
+ }
+
+ @VisibleForTesting
+ static int countPlaceholders(String template) {
+ int index = 0;
+ int count = 0;
+ while (true) {
+ index = template.indexOf("%s", index);
+ if (index == -1) {
+ break;
+ }
+ index++;
+ count++;
+ }
+ return count;
+ }
+
+ static ImmutableList<String> evaluateAll(ImmutableList<LazyMessage> messages) {
+ ImmutableList.Builder<String> result = ImmutableList.builder();
+ for (LazyMessage message : messages) {
+ result.add(message.toString());
+ }
+ return result.build();
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/LongSubject.java b/core/src/main/java/com/google/common/truth/LongSubject.java
new file mode 100644
index 00000000..27f99f54
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/LongSubject.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Propositions for {@code long} subjects.
+ *
+ * @author David Saff
+ * @author Christian Gruber (cgruber@israfil.net)
+ * @author Kurt Alfred Kluever
+ */
+public class LongSubject extends ComparableSubject<Long> {
+ /**
+ * Constructor for use by subclasses. If you want to create an instance of this class itself, call
+ * {@link Subject#check(String, Object...) check(...)}{@code .that(actual)}.
+ */
+ protected LongSubject(FailureMetadata metadata, @Nullable Long actual) {
+ super(metadata, actual);
+ }
+
+ /** @deprecated Use {@link #isEqualTo} instead. Long comparison is consistent with equality. */
+ @Override
+ @Deprecated
+ public final void isEquivalentAccordingToCompareTo(Long other) {
+ super.isEquivalentAccordingToCompareTo(other);
+ }
+
+ /**
+ * Checks that the subject is greater than {@code other}.
+ *
+ * <p>To check that the subject is greater than <i>or equal to</i> {@code other}, use {@link
+ * #isAtLeast}.
+ */
+ public final void isGreaterThan(int other) {
+ isGreaterThan((long) other);
+ }
+
+ /**
+ * Checks that the subject is less than {@code other}.
+ *
+ * <p>To check that the subject is less than <i>or equal to</i> {@code other}, use {@link
+ * #isAtMost} .
+ */
+ public final void isLessThan(int other) {
+ isLessThan((long) other);
+ }
+
+ /**
+ * Checks that the subject is less than or equal to {@code other}.
+ *
+ * <p>To check that the subject is <i>strictly</i> less than {@code other}, use {@link
+ * #isLessThan}.
+ */
+ public final void isAtMost(int other) {
+ isAtMost((long) other);
+ }
+
+ /**
+ * Checks that the subject is greater than or equal to {@code other}.
+ *
+ * <p>To check that the subject is <i>strictly</i> greater than {@code other}, use {@link
+ * #isGreaterThan}.
+ */
+ public final void isAtLeast(int other) {
+ isAtLeast((long) other);
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/MapSubject.java b/core/src/main/java/com/google/common/truth/MapSubject.java
new file mode 100644
index 00000000..6315a8e8
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/MapSubject.java
@@ -0,0 +1,788 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Strings.lenientFormat;
+import static com.google.common.collect.Maps.immutableEntry;
+import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.Fact.simpleFact;
+import static com.google.common.truth.SubjectUtils.countDuplicatesAndAddTypeInfo;
+import static com.google.common.truth.SubjectUtils.hasMatchingToStringPair;
+import static com.google.common.truth.SubjectUtils.objectToTypeName;
+import static com.google.common.truth.SubjectUtils.retainMatchingToString;
+import static java.util.Collections.singletonList;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.LinkedHashMultiset;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multiset;
+import com.google.common.collect.Sets;
+import com.google.common.truth.Correspondence.DiffFormatter;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Propositions for {@link Map} subjects.
+ *
+ * @author Christian Gruber
+ * @author Kurt Alfred Kluever
+ */
+public class MapSubject extends Subject {
+ private final Map<?, ?> actual;
+
+ /**
+ * Constructor for use by subclasses. If you want to create an instance of this class itself, call
+ * {@link Subject#check(String, Object...) check(...)}{@code .that(actual)}.
+ */
+ protected MapSubject(FailureMetadata metadata, @Nullable Map<?, ?> map) {
+ super(metadata, map);
+ this.actual = map;
+ }
+
+ @Override
+ public final void isEqualTo(@Nullable Object other) {
+ if (Objects.equal(actual, other)) {
+ return;
+ }
+
+ // Fail but with a more descriptive message:
+
+ if (actual == null || !(other instanceof Map)) {
+ super.isEqualTo(other);
+ return;
+ }
+
+ containsEntriesInAnyOrder((Map<?, ?>) other, /* allowUnexpected= */ false);
+ }
+
+ /** Fails if the map is not empty. */
+ public final void isEmpty() {
+ if (!actual.isEmpty()) {
+ failWithActual(simpleFact("expected to be empty"));
+ }
+ }
+
+ /** Fails if the map is empty. */
+ public final void isNotEmpty() {
+ if (actual.isEmpty()) {
+ failWithoutActual(simpleFact("expected not to be empty"));
+ }
+ }
+
+ /** Fails if the map does not have the given size. */
+ public final void hasSize(int expectedSize) {
+ checkArgument(expectedSize >= 0, "expectedSize (%s) must be >= 0", expectedSize);
+ check("size()").that(actual.size()).isEqualTo(expectedSize);
+ }
+
+ /** Fails if the map does not contain the given key. */
+ public final void containsKey(@Nullable Object key) {
+ check("keySet()").that(actual.keySet()).contains(key);
+ }
+
+ /** Fails if the map contains the given key. */
+ public final void doesNotContainKey(@Nullable Object key) {
+ check("keySet()").that(actual.keySet()).doesNotContain(key);
+ }
+
+ /** Fails if the map does not contain the given entry. */
+ public final void containsEntry(@Nullable Object key, @Nullable Object value) {
+ Map.Entry<Object, Object> entry = immutableEntry(key, value);
+ if (!actual.entrySet().contains(entry)) {
+ List<Object> keyList = singletonList(key);
+ List<Object> valueList = singletonList(value);
+ if (actual.containsKey(key)) {
+ Object actualValue = actual.get(key);
+ /*
+ * In the case of a null expected or actual value, clarify that the key *is* present and
+ * *is* expected to be present. That is, get() isn't returning null to indicate that the key
+ * is missing, and the user isn't making an assertion that the key is missing.
+ */
+ StandardSubjectBuilder check = check("get(%s)", key);
+ if (value == null || actualValue == null) {
+ check = check.withMessage("key is present but with a different value");
+ }
+ // See the comment on IterableSubject's use of failEqualityCheckForEqualsWithoutDescription.
+ check.that(actualValue).failEqualityCheckForEqualsWithoutDescription(value);
+ } else if (hasMatchingToStringPair(actual.keySet(), keyList)) {
+ failWithoutActual(
+ fact("expected to contain entry", entry),
+ fact("an instance of", objectToTypeName(entry)),
+ simpleFact("but did not"),
+ fact(
+ "though it did contain keys",
+ countDuplicatesAndAddTypeInfo(
+ retainMatchingToString(actual.keySet(), /* itemsToCheck= */ keyList))),
+ fact("full contents", actualCustomStringRepresentationForPackageMembersToCall()));
+ } else if (actual.containsValue(value)) {
+ Set<Object> keys = new LinkedHashSet<>();
+ for (Map.Entry<?, ?> actualEntry : actual.entrySet()) {
+ if (Objects.equal(actualEntry.getValue(), value)) {
+ keys.add(actualEntry.getKey());
+ }
+ }
+ failWithoutActual(
+ fact("expected to contain entry", entry),
+ simpleFact("but did not"),
+ fact("though it did contain keys with that value", keys),
+ fact("full contents", actualCustomStringRepresentationForPackageMembersToCall()));
+ } else if (hasMatchingToStringPair(actual.values(), valueList)) {
+ failWithoutActual(
+ fact("expected to contain entry", entry),
+ fact("an instance of", objectToTypeName(entry)),
+ simpleFact("but did not"),
+ fact(
+ "though it did contain values",
+ countDuplicatesAndAddTypeInfo(
+ retainMatchingToString(actual.values(), /* itemsToCheck= */ valueList))),
+ fact("full contents", actualCustomStringRepresentationForPackageMembersToCall()));
+ } else {
+ failWithActual("expected to contain entry", entry);
+ }
+ }
+ }
+
+ /** Fails if the map contains the given entry. */
+ public final void doesNotContainEntry(@Nullable Object key, @Nullable Object value) {
+ checkNoNeedToDisplayBothValues("entrySet()")
+ .that(actual.entrySet())
+ .doesNotContain(immutableEntry(key, value));
+ }
+
+ /** Fails if the map is not empty. */
+ @CanIgnoreReturnValue
+ public final Ordered containsExactly() {
+ return containsExactlyEntriesIn(ImmutableMap.of());
+ }
+
+ /**
+ * Fails if the map does not contain exactly the given set of key/value pairs.
+ *
+ * <p><b>Warning:</b> the use of varargs means that we cannot guarantee an equal number of
+ * key/value pairs at compile time. Please make sure you provide varargs in key/value pairs!
+ *
+ * <p>The arguments must not contain duplicate keys.
+ */
+ @CanIgnoreReturnValue
+ public final Ordered containsExactly(
+ @Nullable Object k0, @Nullable Object v0, /*@Nullable*/ Object... rest) {
+ return containsExactlyEntriesIn(accumulateMap("containsExactly", k0, v0, rest));
+ }
+
+ @CanIgnoreReturnValue
+ public final Ordered containsAtLeast(
+ @Nullable Object k0, @Nullable Object v0, /*@Nullable*/ Object... rest) {
+ return containsAtLeastEntriesIn(accumulateMap("containsAtLeast", k0, v0, rest));
+ }
+
+ private static Map<Object, Object> accumulateMap(
+ String functionName, @Nullable Object k0, @Nullable Object v0, /*@Nullable*/ Object... rest) {
+ checkArgument(
+ rest.length % 2 == 0,
+ "There must be an equal number of key/value pairs "
+ + "(i.e., the number of key/value parameters (%s) must be even).",
+ rest.length + 2);
+
+ Map<Object, Object> expectedMap = Maps.newLinkedHashMap();
+ expectedMap.put(k0, v0);
+ Multiset<Object> keys = LinkedHashMultiset.create();
+ keys.add(k0);
+ for (int i = 0; i < rest.length; i += 2) {
+ Object key = rest[i];
+ expectedMap.put(key, rest[i + 1]);
+ keys.add(key);
+ }
+ checkArgument(
+ keys.size() == expectedMap.size(),
+ "Duplicate keys (%s) cannot be passed to %s().",
+ keys,
+ functionName);
+ return expectedMap;
+ }
+
+ /** Fails if the map does not contain exactly the given set of entries in the given map. */
+ @CanIgnoreReturnValue
+ public final Ordered containsExactlyEntriesIn(Map<?, ?> expectedMap) {
+ if (expectedMap.isEmpty()) {
+ if (actual.isEmpty()) {
+ return IN_ORDER;
+ } else {
+ isEmpty(); // fails
+ return ALREADY_FAILED;
+ }
+ }
+ boolean containsAnyOrder = containsEntriesInAnyOrder(expectedMap, /* allowUnexpected= */ false);
+ if (containsAnyOrder) {
+ return new MapInOrder(
+ expectedMap, /* allowUnexpected = */ false, /* correspondence = */ null);
+ } else {
+ return ALREADY_FAILED;
+ }
+ }
+
+ /** Fails if the map does not contain at least the given set of entries in the given map. */
+ @CanIgnoreReturnValue
+ public final Ordered containsAtLeastEntriesIn(Map<?, ?> expectedMap) {
+ if (expectedMap.isEmpty()) {
+ return IN_ORDER;
+ }
+ boolean containsAnyOrder = containsEntriesInAnyOrder(expectedMap, /* allowUnexpected= */ true);
+ if (containsAnyOrder) {
+ return new MapInOrder(expectedMap, /* allowUnexpected = */ true, /* correspondence = */ null);
+ } else {
+ return ALREADY_FAILED;
+ }
+ }
+
+ @CanIgnoreReturnValue
+ private boolean containsEntriesInAnyOrder(Map<?, ?> expectedMap, boolean allowUnexpected) {
+ MapDifference<Object, Object, Object> diff =
+ MapDifference.create(actual, expectedMap, allowUnexpected, EQUALITY);
+ if (diff.isEmpty()) {
+ return true;
+ }
+ // TODO(cpovirk): Consider adding a special-case where the diff contains exactly one key which
+ // is present with the wrong value, doing an isEqualTo assertion on the values. Pro: This gives
+ // us all the extra power of isEqualTo, including maybe throwing a ComparisonFailure. Con: It
+ // might be misleading to report a single mismatched value when the assertion was on the whole
+ // map - this could be mitigated by adding extra info explaining that. (Would need to ensure
+ // that it still fails in cases where e.g. the value is 1 and it should be 1L, where isEqualTo
+ // succeeds: perhaps failEqualityCheckForEqualsWithoutDescription will do the right thing.)
+ // First, we need to decide whether this kind of cleverness is a line we want to cross.
+ // (See also containsEntry, which does do an isEqualTo-like assertion when the expected key is
+ // present with the wrong value, which may be the closest we currently get to this.)
+ failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .addAll(diff.describe(/* differ = */ null))
+ .add(simpleFact("---"))
+ .add(fact(allowUnexpected ? "expected to contain at least" : "expected", expectedMap))
+ .add(butWas())
+ .build());
+ return false;
+ }
+
+ private interface ValueTester<A, E> {
+ boolean test(@Nullable A actualValue, @Nullable E expectedValue);
+ }
+
+ @SuppressWarnings("UnnecessaryAnonymousClass") // for Java 7 compatibility
+ private static final ValueTester<Object, Object> EQUALITY =
+ new ValueTester<Object, Object>() {
+ @Override
+ public boolean test(@Nullable Object actualValue, @Nullable Object expectedValue) {
+ return Objects.equal(actualValue, expectedValue);
+ }
+ };
+
+ private interface Differ<A, E> {
+ String diff(A actual, E expected);
+ }
+
+ // This is mostly like the MapDifference code in com.google.common.collect, generalized to remove
+ // the requirement that the values of the two maps are of the same type and are compared with a
+ // symmetric Equivalence.
+ private static class MapDifference<K, A, E> {
+ private final Map<K, E> missing;
+ private final Map<K, A> unexpected;
+ private final Map<K, ValueDifference<A, E>> wrongValues;
+ private final Set<K> allKeys;
+
+ static <K, A, E> MapDifference<K, A, E> create(
+ Map<? extends K, ? extends A> actual,
+ Map<? extends K, ? extends E> expected,
+ boolean allowUnexpected,
+ ValueTester<? super A, ? super E> valueTester) {
+ Map<K, A> unexpected = new LinkedHashMap<>(actual);
+ Map<K, E> missing = new LinkedHashMap<>();
+ Map<K, ValueDifference<A, E>> wrongValues = new LinkedHashMap<>();
+ for (Map.Entry<? extends K, ? extends E> expectedEntry : expected.entrySet()) {
+ K expectedKey = expectedEntry.getKey();
+ E expectedValue = expectedEntry.getValue();
+ if (actual.containsKey(expectedKey)) {
+ A actualValue = unexpected.remove(expectedKey);
+ if (!valueTester.test(actualValue, expectedValue)) {
+ wrongValues.put(expectedKey, new ValueDifference<>(actualValue, expectedValue));
+ }
+ } else {
+ missing.put(expectedKey, expectedValue);
+ }
+ }
+ if (allowUnexpected) {
+ unexpected.clear();
+ }
+ return new MapDifference<>(
+ missing, unexpected, wrongValues, Sets.union(actual.keySet(), expected.keySet()));
+ }
+
+ private MapDifference(
+ Map<K, E> missing,
+ Map<K, A> unexpected,
+ Map<K, ValueDifference<A, E>> wrongValues,
+ Set<K> allKeys) {
+ this.missing = missing;
+ this.unexpected = unexpected;
+ this.wrongValues = wrongValues;
+ this.allKeys = allKeys;
+ }
+
+ boolean isEmpty() {
+ return missing.isEmpty() && unexpected.isEmpty() && wrongValues.isEmpty();
+ }
+
+ ImmutableList<Fact> describe(@Nullable Differ<? super A, ? super E> differ) {
+ boolean includeKeyTypes = includeKeyTypes();
+ ImmutableList.Builder<Fact> facts = ImmutableList.builder();
+ if (!wrongValues.isEmpty()) {
+ facts.add(simpleFact("keys with wrong values"));
+ }
+ for (Map.Entry<K, ValueDifference<A, E>> entry : wrongValues.entrySet()) {
+ facts.add(fact("for key", maybeAddType(entry.getKey(), includeKeyTypes)));
+ facts.addAll(entry.getValue().describe(differ));
+ }
+ if (!missing.isEmpty()) {
+ facts.add(simpleFact("missing keys"));
+ }
+ for (Map.Entry<K, E> entry : missing.entrySet()) {
+ facts.add(fact("for key", maybeAddType(entry.getKey(), includeKeyTypes)));
+ facts.add(fact("expected value", entry.getValue()));
+ }
+ if (!unexpected.isEmpty()) {
+ facts.add(simpleFact("unexpected keys"));
+ }
+ for (Map.Entry<K, A> entry : unexpected.entrySet()) {
+ facts.add(fact("for key", maybeAddType(entry.getKey(), includeKeyTypes)));
+ facts.add(fact("unexpected value", entry.getValue()));
+ }
+ return facts.build();
+ }
+
+ private boolean includeKeyTypes() {
+ // We will annotate all the keys in the diff with their types if any of the keys involved have
+ // the same toString() without being equal.
+ Set<K> keys = Sets.newHashSet();
+ keys.addAll(missing.keySet());
+ keys.addAll(unexpected.keySet());
+ keys.addAll(wrongValues.keySet());
+ return hasMatchingToStringPair(keys, allKeys);
+ }
+ }
+
+ private static class ValueDifference<A, E> {
+ private final A actual;
+ private final E expected;
+
+ ValueDifference(@Nullable A actual, @Nullable E expected) {
+ this.actual = actual;
+ this.expected = expected;
+ }
+
+ ImmutableList<Fact> describe(@Nullable Differ<? super A, ? super E> differ) {
+ boolean includeTypes =
+ differ == null && String.valueOf(actual).equals(String.valueOf(expected));
+ ImmutableList.Builder<Fact> facts =
+ ImmutableList.<Fact>builder()
+ .add(fact("expected value", maybeAddType(expected, includeTypes)))
+ .add(fact("but got value", maybeAddType(actual, includeTypes)));
+
+ if (differ != null) {
+ String diffString = differ.diff(actual, expected);
+ if (diffString != null) {
+ facts.add(fact("diff", diffString));
+ }
+ }
+ return facts.build();
+ }
+ }
+
+ private static String maybeAddType(Object object, boolean includeTypes) {
+ return includeTypes
+ ? lenientFormat("%s (%s)", object, objectToTypeName(object))
+ : String.valueOf(object);
+ }
+
+ private class MapInOrder implements Ordered {
+
+ private final Map<?, ?> expectedMap;
+ private final boolean allowUnexpected;
+ private final @Nullable Correspondence<?, ?> correspondence;
+
+ MapInOrder(
+ Map<?, ?> expectedMap,
+ boolean allowUnexpected,
+ @Nullable Correspondence<?, ?> correspondence) {
+ this.expectedMap = expectedMap;
+ this.allowUnexpected = allowUnexpected;
+ this.correspondence = correspondence;
+ }
+
+ /**
+ * Checks whether the common elements between actual and expected are in the same order.
+ *
+ * <p>This doesn't check whether the keys have the same values or whether all the required keys
+ * are actually present. That was supposed to be done before the "in order" part.
+ */
+ @Override
+ public void inOrder() {
+ // We're using the fact that Sets.intersection keeps the order of the first set.
+ List<?> expectedKeyOrder =
+ Lists.newArrayList(Sets.intersection(expectedMap.keySet(), actual.keySet()));
+ List<?> actualKeyOrder =
+ Lists.newArrayList(Sets.intersection(actual.keySet(), expectedMap.keySet()));
+ if (!actualKeyOrder.equals(expectedKeyOrder)) {
+ ImmutableList.Builder<Fact> facts =
+ ImmutableList.<Fact>builder()
+ .add(
+ simpleFact(
+ allowUnexpected
+ ? "required entries were all found, but order was wrong"
+ : "entries match, but order was wrong"))
+ .add(
+ fact(
+ allowUnexpected ? "expected to contain at least" : "expected",
+ expectedMap));
+ if (correspondence != null) {
+ facts.addAll(correspondence.describeForMapValues());
+ }
+ failWithActual(facts.build());
+ }
+ }
+ }
+
+ /** Ordered implementation that does nothing because it's already known to be true. */
+ @SuppressWarnings("UnnecessaryAnonymousClass") // for Java 7 compatibility
+ private static final Ordered IN_ORDER =
+ new Ordered() {
+ @Override
+ public void inOrder() {}
+ };
+
+ /** Ordered implementation that does nothing because an earlier check already caused a failure. */
+ @SuppressWarnings("UnnecessaryAnonymousClass") // for Java 7 compatibility
+ private static final Ordered ALREADY_FAILED =
+ new Ordered() {
+ @Override
+ public void inOrder() {}
+ };
+
+ /**
+ * Starts a method chain for a check in which the actual values (i.e. the values of the {@link
+ * Map} under test) are compared to expected values using the given {@link Correspondence}. The
+ * actual values must be of type {@code A}, the expected values must be of type {@code E}. The
+ * check is actually executed by continuing the method chain. For example:
+ *
+ * <pre>{@code
+ * assertThat(actualMap)
+ * .comparingValuesUsing(correspondence)
+ * .containsEntry(expectedKey, expectedValue);
+ * }</pre>
+ *
+ * where {@code actualMap} is a {@code Map<?, A>} (or, more generally, a {@code Map<?, ? extends
+ * A>}), {@code correspondence} is a {@code Correspondence<A, E>}, and {@code expectedValue} is an
+ * {@code E}.
+ *
+ * <p>Note that keys will always be compared with regular object equality ({@link Object#equals}).
+ *
+ * <p>Any of the methods on the returned object may throw {@link ClassCastException} if they
+ * encounter an actual value that is not of type {@code A} or an expected value that is not of
+ * type {@code E}.
+ */
+ public final <A, E> UsingCorrespondence<A, E> comparingValuesUsing(
+ Correspondence<? super A, ? super E> correspondence) {
+ return new UsingCorrespondence<>(correspondence);
+ }
+
+ /**
+ * Starts a method chain for a check in which failure messages may use the given {@link
+ * DiffFormatter} to describe the difference between an actual value (i.e. a value in the {@link
+ * Map} under test) and the value it is expected to be equal to, but isn't. The actual and
+ * expected values must be of type {@code V}. The check is actually executed by continuing the
+ * method chain. For example:
+ *
+ * <pre>{@code
+ * assertThat(actualMap)
+ * .formattingDiffsUsing(FooTestHelper::formatDiff)
+ * .containsExactly(key1, foo1, key2, foo2, key3, foo3);
+ * }</pre>
+ *
+ * where {@code actualMap} is a {@code Map<?, Foo>} (or, more generally, a {@code Map<?, ? extends
+ * Foo>}), {@code FooTestHelper.formatDiff} is a static method taking two {@code Foo} arguments
+ * and returning a {@link String}, and {@code foo1}, {@code foo2}, and {@code foo3} are {@code
+ * Foo} instances.
+ *
+ * <p>Unlike when using {@link #comparingValuesUsing}, the values are still compared using object
+ * equality, so this method does not affect whether a test passes or fails.
+ *
+ * <p>Any of the methods on the returned object may throw {@link ClassCastException} if they
+ * encounter a value that is not of type {@code V}.
+ *
+ * @since 1.1
+ */
+ public final <V> UsingCorrespondence<V, V> formattingDiffsUsing(
+ DiffFormatter<? super V, ? super V> formatter) {
+ return comparingValuesUsing(Correspondence.<V>equality().formattingDiffsUsing(formatter));
+ }
+
+ /**
+ * A partially specified check in which the actual values (i.e. the values of the {@link Map}
+ * under test) are compared to expected values using a {@link Correspondence}. The expected values
+ * are of type {@code E}. Call methods on this object to actually execute the check.
+ *
+ * <p>Note that keys will always be compared with regular object equality ({@link Object#equals}).
+ */
+ public final class UsingCorrespondence<A, E> {
+
+ private final Correspondence<? super A, ? super E> correspondence;
+
+ private UsingCorrespondence(Correspondence<? super A, ? super E> correspondence) {
+ this.correspondence = checkNotNull(correspondence);
+ }
+
+ /**
+ * Fails if the map does not contain an entry with the given key and a value that corresponds to
+ * the given value.
+ */
+ public void containsEntry(@Nullable Object expectedKey, @Nullable E expectedValue) {
+ if (actual.containsKey(expectedKey)) {
+ // Found matching key.
+ A actualValue = getCastSubject().get(expectedKey);
+ Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forMapValues();
+ if (correspondence.safeCompare(actualValue, expectedValue, exceptions)) {
+ // The expected key had the expected value. There's no need to check exceptions here,
+ // because if Correspondence.compare() threw then safeCompare() would return false.
+ return;
+ }
+ // Found matching key with non-matching value.
+ String diff = correspondence.safeFormatDiff(actualValue, expectedValue, exceptions);
+ if (diff != null) {
+ failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .add(fact("for key", expectedKey))
+ .add(fact("expected value", expectedValue))
+ .addAll(correspondence.describeForMapValues())
+ .add(fact("but got value", actualValue))
+ .add(fact("diff", diff))
+ .add(fact("full map", actualCustomStringRepresentationForPackageMembersToCall()))
+ .addAll(exceptions.describeAsAdditionalInfo())
+ .build());
+ } else {
+ failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .add(fact("for key", expectedKey))
+ .add(fact("expected value", expectedValue))
+ .addAll(correspondence.describeForMapValues())
+ .add(fact("but got value", actualValue))
+ .add(fact("full map", actualCustomStringRepresentationForPackageMembersToCall()))
+ .addAll(exceptions.describeAsAdditionalInfo())
+ .build());
+ }
+ } else {
+ // Did not find matching key. Look for the matching value with a different key.
+ Set<Object> keys = new LinkedHashSet<>();
+ Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forMapValues();
+ for (Map.Entry<?, A> actualEntry : getCastSubject().entrySet()) {
+ if (correspondence.safeCompare(actualEntry.getValue(), expectedValue, exceptions)) {
+ keys.add(actualEntry.getKey());
+ }
+ }
+ if (!keys.isEmpty()) {
+ // Found matching values with non-matching keys.
+ failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .add(fact("for key", expectedKey))
+ .add(fact("expected value", expectedValue))
+ .addAll(correspondence.describeForMapValues())
+ .add(simpleFact("but was missing"))
+ .add(fact("other keys with matching values", keys))
+ .add(fact("full map", actualCustomStringRepresentationForPackageMembersToCall()))
+ .addAll(exceptions.describeAsAdditionalInfo())
+ .build());
+ } else {
+ // Did not find matching key or value.
+ failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .add(fact("for key", expectedKey))
+ .add(fact("expected value", expectedValue))
+ .addAll(correspondence.describeForMapValues())
+ .add(simpleFact("but was missing"))
+ .add(fact("full map", actualCustomStringRepresentationForPackageMembersToCall()))
+ .addAll(exceptions.describeAsAdditionalInfo())
+ .build());
+ }
+ }
+ }
+
+ /**
+ * Fails if the map contains an entry with the given key and a value that corresponds to the
+ * given value.
+ */
+ public void doesNotContainEntry(@Nullable Object excludedKey, @Nullable E excludedValue) {
+ if (actual.containsKey(excludedKey)) {
+ // Found matching key. Fail if the value matches, too.
+ A actualValue = getCastSubject().get(excludedKey);
+ Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forMapValues();
+ if (correspondence.safeCompare(actualValue, excludedValue, exceptions)) {
+ // The matching key had a matching value. There's no need to check exceptions here,
+ // because if Correspondence.compare() threw then safeCompare() would return false.
+ failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .add(fact("expected not to contain", immutableEntry(excludedKey, excludedValue)))
+ .addAll(correspondence.describeForMapValues())
+ .add(fact("but contained", immutableEntry(excludedKey, actualValue)))
+ .add(fact("full map", actualCustomStringRepresentationForPackageMembersToCall()))
+ .addAll(exceptions.describeAsAdditionalInfo())
+ .build());
+ }
+ // The value didn't match, but we still need to fail if we hit an exception along the way.
+ if (exceptions.hasCompareException()) {
+ failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .addAll(exceptions.describeAsMainCause())
+ .add(fact("expected not to contain", immutableEntry(excludedKey, excludedValue)))
+ .addAll(correspondence.describeForMapValues())
+ .add(simpleFact("found no match (but failing because of exception)"))
+ .add(fact("full map", actualCustomStringRepresentationForPackageMembersToCall()))
+ .build());
+ }
+ }
+ }
+
+ /**
+ * Fails if the map does not contain exactly the given set of keys mapping to values that
+ * correspond to the given values.
+ *
+ * <p>The values must all be of type {@code E}, and a {@link ClassCastException} will be thrown
+ * if any other type is encountered.
+ *
+ * <p><b>Warning:</b> the use of varargs means that we cannot guarantee an equal number of
+ * key/value pairs at compile time. Please make sure you provide varargs in key/value pairs!
+ */
+ // TODO(b/25744307): Can we add an error-prone check that rest.length % 2 == 0?
+ // For bonus points, checking that the even-numbered values are of type E would be sweet.
+ @CanIgnoreReturnValue
+ public Ordered containsExactly(
+ @Nullable Object k0, @Nullable E v0, /*@Nullable*/ Object... rest) {
+ @SuppressWarnings("unchecked") // throwing ClassCastException is the correct behaviour
+ Map<Object, E> expectedMap = (Map<Object, E>) accumulateMap("containsExactly", k0, v0, rest);
+ return containsExactlyEntriesIn(expectedMap);
+ }
+
+ /**
+ * Fails if the map does not contain at least the given set of keys mapping to values that
+ * correspond to the given values.
+ *
+ * <p>The values must all be of type {@code E}, and a {@link ClassCastException} will be thrown
+ * if any other type is encountered.
+ *
+ * <p><b>Warning:</b> the use of varargs means that we cannot guarantee an equal number of
+ * key/value pairs at compile time. Please make sure you provide varargs in key/value pairs!
+ */
+ // TODO(b/25744307): Can we add an error-prone check that rest.length % 2 == 0?
+ // For bonus points, checking that the even-numbered values are of type E would be sweet.
+ @CanIgnoreReturnValue
+ public Ordered containsAtLeast(
+ @Nullable Object k0, @Nullable E v0, /*@Nullable*/ Object... rest) {
+ @SuppressWarnings("unchecked") // throwing ClassCastException is the correct behaviour
+ Map<Object, E> expectedMap = (Map<Object, E>) accumulateMap("containsAtLeast", k0, v0, rest);
+ return containsAtLeastEntriesIn(expectedMap);
+ }
+
+ /**
+ * Fails if the map does not contain exactly the keys in the given map, mapping to values that
+ * correspond to the values of the given map.
+ */
+ @CanIgnoreReturnValue
+ public Ordered containsExactlyEntriesIn(Map<?, ? extends E> expectedMap) {
+ if (expectedMap.isEmpty()) {
+ if (actual.isEmpty()) {
+ return IN_ORDER;
+ } else {
+ isEmpty(); // fails
+ return ALREADY_FAILED;
+ }
+ }
+ return internalContainsEntriesIn(expectedMap, /* allowUnexpected = */ false);
+ }
+
+ /**
+ * Fails if the map does not contain at least the keys in the given map, mapping to values that
+ * correspond to the values of the given map.
+ */
+ @CanIgnoreReturnValue
+ public Ordered containsAtLeastEntriesIn(Map<?, ? extends E> expectedMap) {
+ if (expectedMap.isEmpty()) {
+ return IN_ORDER;
+ }
+ return internalContainsEntriesIn(expectedMap, /* allowUnexpected = */ true);
+ }
+
+ private <K, V extends E> Ordered internalContainsEntriesIn(
+ Map<K, V> expectedMap, boolean allowUnexpected) {
+ final Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forMapValues();
+ MapDifference<Object, A, V> diff =
+ MapDifference.create(
+ getCastSubject(),
+ expectedMap,
+ allowUnexpected,
+ new ValueTester<A, E>() {
+ @Override
+ public boolean test(A actualValue, E expectedValue) {
+ return correspondence.safeCompare(actualValue, expectedValue, exceptions);
+ }
+ });
+ if (diff.isEmpty()) {
+ // The maps correspond exactly. There's no need to check exceptions here, because if
+ // Correspondence.compare() threw then safeCompare() would return false and the diff would
+ // record that we had the wrong value for that key.
+ return new MapInOrder(expectedMap, allowUnexpected, correspondence);
+ }
+ failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .addAll(diff.describe(differ(exceptions)))
+ .add(simpleFact("---"))
+ .add(fact(allowUnexpected ? "expected to contain at least" : "expected", expectedMap))
+ .addAll(correspondence.describeForMapValues())
+ .add(butWas())
+ .addAll(exceptions.describeAsAdditionalInfo())
+ .build());
+ return ALREADY_FAILED;
+ }
+
+ @SuppressWarnings("UnnecessaryAnonymousClass") // for Java 7 compatibility
+ private <V extends E> Differ<A, V> differ(final Correspondence.ExceptionStore exceptions) {
+ return new Differ<A, V>() {
+ @Override
+ public String diff(A actual, V expected) {
+ return correspondence.safeFormatDiff(actual, expected, exceptions);
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked") // throwing ClassCastException is the correct behaviour
+ private Map<?, A> getCastSubject() {
+ return (Map<?, A>) actual;
+ }
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/MathUtil.java b/core/src/main/java/com/google/common/truth/MathUtil.java
new file mode 100644
index 00000000..791ac4d7
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/MathUtil.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import com.google.common.primitives.Doubles;
+
+/** Math utilities to be shared by numeric subjects. */
+final class MathUtil {
+ private MathUtil() {}
+
+ /**
+ * Returns true iff {@code left} and {@code right} are finite values within {@code tolerance} of
+ * each other. Note that both this method and {@link #notEqualWithinTolerance} returns false if
+ * either {@code left} or {@code right} is infinite or NaN.
+ */
+ public static boolean equalWithinTolerance(double left, double right, double tolerance) {
+ return Math.abs(left - right) <= Math.abs(tolerance);
+ }
+
+ /**
+ * Returns true iff {@code left} and {@code right} are finite values within {@code tolerance} of
+ * each other. Note that both this method and {@link #notEqualWithinTolerance} returns false if
+ * either {@code left} or {@code right} is infinite or NaN.
+ */
+ public static boolean equalWithinTolerance(float left, float right, float tolerance) {
+ return equalWithinTolerance((double) left, (double) right, (double) tolerance);
+ }
+
+ /**
+ * Returns true iff {@code left} and {@code right} are finite values not within {@code tolerance}
+ * of each other. Note that both this method and {@link #equalWithinTolerance} returns false if
+ * either {@code left} or {@code right} is infinite or NaN.
+ */
+ public static boolean notEqualWithinTolerance(double left, double right, double tolerance) {
+ if (Doubles.isFinite(left) && Doubles.isFinite(right)) {
+ return Math.abs(left - right) > Math.abs(tolerance);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns true iff {@code left} and {@code right} are finite values not within {@code tolerance}
+ * of each other. Note that both this method and {@link #equalWithinTolerance} returns false if
+ * either {@code left} or {@code right} is infinite or NaN.
+ */
+ public static boolean notEqualWithinTolerance(float left, float right, float tolerance) {
+ return notEqualWithinTolerance((double) left, (double) right, (double) tolerance);
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/MultimapSubject.java b/core/src/main/java/com/google/common/truth/MultimapSubject.java
new file mode 100644
index 00000000..332da4ae
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/MultimapSubject.java
@@ -0,0 +1,828 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Strings.lenientFormat;
+import static com.google.common.collect.Maps.immutableEntry;
+import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.Fact.simpleFact;
+import static com.google.common.truth.SubjectUtils.HUMAN_UNDERSTANDABLE_EMPTY_STRING;
+import static com.google.common.truth.SubjectUtils.countDuplicatesAndAddTypeInfo;
+import static com.google.common.truth.SubjectUtils.hasMatchingToStringPair;
+import static com.google.common.truth.SubjectUtils.objectToTypeName;
+import static com.google.common.truth.SubjectUtils.retainMatchingToString;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.LinkedHashMultiset;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.SetMultimap;
+import com.google.common.collect.Sets;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Propositions for {@link Multimap} subjects.
+ *
+ * @author Daniel Ploch
+ * @author Kurt Alfred Kluever
+ */
+public class MultimapSubject extends Subject {
+
+ /** Ordered implementation that does nothing because an earlier check already caused a failure. */
+ @SuppressWarnings("UnnecessaryAnonymousClass") // for Java 7 compatibility
+ private static final Ordered ALREADY_FAILED =
+ new Ordered() {
+ @Override
+ public void inOrder() {}
+ };
+
+ private final Multimap<?, ?> actual;
+
+ /**
+ * Constructor for use by subclasses. If you want to create an instance of this class itself, call
+ * {@link Subject#check(String, Object...) check(...)}{@code .that(actual)}.
+ */
+ protected MultimapSubject(FailureMetadata metadata, @Nullable Multimap<?, ?> multimap) {
+ this(metadata, multimap, null);
+ }
+
+ MultimapSubject(
+ FailureMetadata metadata,
+ @Nullable Multimap<?, ?> multimap,
+ @Nullable String typeDescription) {
+ super(metadata, multimap, typeDescription);
+ this.actual = multimap;
+ }
+
+ /** Fails if the multimap is not empty. */
+ public final void isEmpty() {
+ if (!actual.isEmpty()) {
+ failWithActual(simpleFact("expected to be empty"));
+ }
+ }
+
+ /** Fails if the multimap is empty. */
+ public final void isNotEmpty() {
+ if (actual.isEmpty()) {
+ failWithoutActual(simpleFact("expected not to be empty"));
+ }
+ }
+
+ /** Fails if the multimap does not have the given size. */
+ public final void hasSize(int expectedSize) {
+ checkArgument(expectedSize >= 0, "expectedSize(%s) must be >= 0", expectedSize);
+ check("size()").that(actual.size()).isEqualTo(expectedSize);
+ }
+
+ /** Fails if the multimap does not contain the given key. */
+ public final void containsKey(@Nullable Object key) {
+ check("keySet()").that(actual.keySet()).contains(key);
+ }
+
+ /** Fails if the multimap contains the given key. */
+ public final void doesNotContainKey(@Nullable Object key) {
+ check("keySet()").that(actual.keySet()).doesNotContain(key);
+ }
+
+ /** Fails if the multimap does not contain the given entry. */
+ public final void containsEntry(@Nullable Object key, @Nullable Object value) {
+ // TODO(kak): Can we share any of this logic w/ MapSubject.containsEntry()?
+ if (!actual.containsEntry(key, value)) {
+ Map.Entry<Object, Object> entry = immutableEntry(key, value);
+ List<Map.Entry<Object, Object>> entryList = ImmutableList.of(entry);
+ // TODO(cpovirk): If the key is present but not with the right value, we could fail using
+ // something like valuesForKey(key).contains(value). Consider whether this is worthwhile.
+ if (hasMatchingToStringPair(actual.entries(), entryList)) {
+ failWithoutActual(
+ fact("expected to contain entry", entry),
+ fact("an instance of", objectToTypeName(entry)),
+ simpleFact("but did not"),
+ fact(
+ "though it did contain",
+ countDuplicatesAndAddTypeInfo(
+ retainMatchingToString(actual.entries(), /* itemsToCheck = */ entryList))),
+ fact("full contents", actualCustomStringRepresentationForPackageMembersToCall()));
+ } else if (actual.containsKey(key)) {
+ failWithoutActual(
+ fact("expected to contain entry", entry),
+ simpleFact("but did not"),
+ fact("though it did contain values with that key", actual.asMap().get(key)),
+ fact("full contents", actualCustomStringRepresentationForPackageMembersToCall()));
+ } else if (actual.containsValue(value)) {
+ Set<Object> keys = new LinkedHashSet<>();
+ for (Map.Entry<?, ?> actualEntry : actual.entries()) {
+ if (Objects.equal(actualEntry.getValue(), value)) {
+ keys.add(actualEntry.getKey());
+ }
+ }
+ failWithoutActual(
+ fact("expected to contain entry", entry),
+ simpleFact("but did not"),
+ fact("though it did contain keys with that value", keys),
+ fact("full contents", actualCustomStringRepresentationForPackageMembersToCall()));
+ } else {
+ failWithActual("expected to contain entry", immutableEntry(key, value));
+ }
+ }
+ }
+
+ /** Fails if the multimap contains the given entry. */
+ public final void doesNotContainEntry(@Nullable Object key, @Nullable Object value) {
+ checkNoNeedToDisplayBothValues("entries()")
+ .that(actual.entries())
+ .doesNotContain(immutableEntry(key, value));
+ }
+
+ /**
+ * Returns a context-aware {@link Subject} for making assertions about the values for the given
+ * key within the {@link Multimap}.
+ *
+ * <p>This method performs no checks on its own and cannot cause test failures. Subsequent
+ * assertions must be chained onto this method call to test properties of the {@link Multimap}.
+ */
+ @SuppressWarnings("unchecked") // safe because we only read, not write
+ /*
+ * non-final because it's overridden by MultimapWithProtoValuesSubject.
+ *
+ * If we really, really wanted it to be final, we could investigate whether
+ * MultimapWithProtoValuesFluentAssertion could provide its own valuesForKey method. But that
+ * would force callers to perform any configuration _before_ the valuesForKey call, while
+ * currently they must perform it _after_.
+ */
+ public IterableSubject valuesForKey(@Nullable Object key) {
+ return check("valuesForKey(%s)", key).that(((Multimap<Object, Object>) actual).get(key));
+ }
+
+ @Override
+ public final void isEqualTo(@Nullable Object other) {
+ @SuppressWarnings("UndefinedEquals") // the contract of this method is to follow Multimap.equals
+ boolean isEqual = Objects.equal(actual, other);
+ if (isEqual) {
+ return;
+ }
+
+ // Fail but with a more descriptive message:
+ if ((actual instanceof ListMultimap && other instanceof SetMultimap)
+ || (actual instanceof SetMultimap && other instanceof ListMultimap)) {
+ String actualType = (actual instanceof ListMultimap) ? "ListMultimap" : "SetMultimap";
+ String otherType = (other instanceof ListMultimap) ? "ListMultimap" : "SetMultimap";
+ failWithoutActual(
+ fact("expected", other),
+ fact("an instance of", otherType),
+ fact("but was", actualCustomStringRepresentationForPackageMembersToCall()),
+ fact("an instance of", actualType),
+ simpleFact(
+ lenientFormat(
+ "a %s cannot equal a %s if either is non-empty", actualType, otherType)));
+ } else if (actual instanceof ListMultimap) {
+ containsExactlyEntriesIn((Multimap<?, ?>) other).inOrder();
+ } else if (actual instanceof SetMultimap) {
+ containsExactlyEntriesIn((Multimap<?, ?>) other);
+ } else {
+ super.isEqualTo(other);
+ }
+ }
+
+ /**
+ * Fails if the {@link Multimap} does not contain precisely the same entries as the argument
+ * {@link Multimap}.
+ *
+ * <p>A subsequent call to {@link Ordered#inOrder} may be made if the caller wishes to verify that
+ * the two multimaps iterate fully in the same order. That is, their key sets iterate in the same
+ * order, and the value collections for each key iterate in the same order.
+ */
+ @CanIgnoreReturnValue
+ public final Ordered containsExactlyEntriesIn(Multimap<?, ?> expectedMultimap) {
+ checkNotNull(expectedMultimap, "expectedMultimap");
+ ListMultimap<?, ?> missing = difference(expectedMultimap, actual);
+ ListMultimap<?, ?> extra = difference(actual, expectedMultimap);
+
+ // TODO(kak): Possible enhancement: Include "[1 copy]" if the element does appear in
+ // the subject but not enough times. Similarly for unexpected extra items.
+ if (!missing.isEmpty()) {
+ if (!extra.isEmpty()) {
+ boolean addTypeInfo = hasMatchingToStringPair(missing.entries(), extra.entries());
+ // Note: The usage of countDuplicatesAndAddTypeInfo() below causes entries no longer to be
+ // grouped by key in the 'missing' and 'unexpected items' parts of the message (we still
+ // show the actual and expected multimaps in the standard format).
+ String missingDisplay =
+ addTypeInfo
+ ? countDuplicatesAndAddTypeInfo(annotateEmptyStringsMultimap(missing).entries())
+ : countDuplicatesMultimap(annotateEmptyStringsMultimap(missing));
+ String extraDisplay =
+ addTypeInfo
+ ? countDuplicatesAndAddTypeInfo(annotateEmptyStringsMultimap(extra).entries())
+ : countDuplicatesMultimap(annotateEmptyStringsMultimap(extra));
+ failWithActual(
+ fact("missing", missingDisplay),
+ fact("unexpected", extraDisplay),
+ simpleFact("---"),
+ fact("expected", annotateEmptyStringsMultimap(expectedMultimap)));
+ return ALREADY_FAILED;
+ } else {
+ failWithActual(
+ fact("missing", countDuplicatesMultimap(annotateEmptyStringsMultimap(missing))),
+ simpleFact("---"),
+ fact("expected", annotateEmptyStringsMultimap(expectedMultimap)));
+ return ALREADY_FAILED;
+ }
+ } else if (!extra.isEmpty()) {
+ failWithActual(
+ fact("unexpected", countDuplicatesMultimap(annotateEmptyStringsMultimap(extra))),
+ simpleFact("---"),
+ fact("expected", annotateEmptyStringsMultimap(expectedMultimap)));
+ return ALREADY_FAILED;
+ }
+
+ return new MultimapInOrder(/* allowUnexpected = */ false, expectedMultimap);
+ }
+
+ /**
+ * Fails if the {@link Multimap} does not contain at least the entries in the argument {@link
+ * Multimap}.
+ *
+ * <p>A subsequent call to {@link Ordered#inOrder} may be made if the caller wishes to verify that
+ * the entries are present in the same order as given. That is, the keys are present in the given
+ * order in the key set, and the values for each key are present in the given order order in the
+ * value collections.
+ */
+ @CanIgnoreReturnValue
+ public final Ordered containsAtLeastEntriesIn(Multimap<?, ?> expectedMultimap) {
+ checkNotNull(expectedMultimap, "expectedMultimap");
+ ListMultimap<?, ?> missing = difference(expectedMultimap, actual);
+
+ // TODO(kak): Possible enhancement: Include "[1 copy]" if the element does appear in
+ // the subject but not enough times. Similarly for unexpected extra items.
+ if (!missing.isEmpty()) {
+ failWithActual(
+ fact("missing", countDuplicatesMultimap(annotateEmptyStringsMultimap(missing))),
+ simpleFact("---"),
+ fact("expected to contain at least", annotateEmptyStringsMultimap(expectedMultimap)));
+ return ALREADY_FAILED;
+ }
+
+ return new MultimapInOrder(/* allowUnexpected = */ true, expectedMultimap);
+ }
+
+ /** Fails if the multimap is not empty. */
+ @CanIgnoreReturnValue
+ public final Ordered containsExactly() {
+ return check().about(iterableEntries()).that(actual.entries()).containsExactly();
+ }
+
+ /**
+ * Fails if the multimap does not contain exactly the given set of key/value pairs.
+ *
+ * <p><b>Warning:</b> the use of varargs means that we cannot guarantee an equal number of
+ * key/value pairs at compile time. Please make sure you provide varargs in key/value pairs!
+ */
+ @CanIgnoreReturnValue
+ public final Ordered containsExactly(
+ @Nullable Object k0, @Nullable Object v0, /*@Nullable*/ Object... rest) {
+ return containsExactlyEntriesIn(accumulateMultimap(k0, v0, rest));
+ }
+
+ /**
+ * Fails if the multimap does not contain at least the given key/value pairs.
+ *
+ * <p><b>Warning:</b> the use of varargs means that we cannot guarantee an equal number of
+ * key/value pairs at compile time. Please make sure you provide varargs in key/value pairs!
+ */
+ @CanIgnoreReturnValue
+ public final Ordered containsAtLeast(
+ @Nullable Object k0, @Nullable Object v0, /*@Nullable*/ Object... rest) {
+ return containsAtLeastEntriesIn(accumulateMultimap(k0, v0, rest));
+ }
+
+ private static Multimap<Object, Object> accumulateMultimap(
+ @Nullable Object k0, @Nullable Object v0, /*@Nullable*/ Object... rest) {
+ checkArgument(
+ rest.length % 2 == 0,
+ "There must be an equal number of key/value pairs "
+ + "(i.e., the number of key/value parameters (%s) must be even).",
+ rest.length + 2);
+
+ LinkedListMultimap<Object, Object> expectedMultimap = LinkedListMultimap.create();
+ expectedMultimap.put(k0, v0);
+ for (int i = 0; i < rest.length; i += 2) {
+ expectedMultimap.put(rest[i], rest[i + 1]);
+ }
+ return expectedMultimap;
+ }
+
+ private Factory<IterableSubject, Iterable<?>> iterableEntries() {
+ return new Factory<IterableSubject, Iterable<?>>() {
+ @Override
+ public IterableSubject createSubject(FailureMetadata metadata, Iterable<?> actual) {
+ return new IterableEntries(metadata, MultimapSubject.this, actual);
+ }
+ };
+ }
+
+ private static class IterableEntries extends IterableSubject {
+ private final String stringRepresentation;
+
+ IterableEntries(FailureMetadata metadata, MultimapSubject multimapSubject, Iterable<?> actual) {
+ super(metadata, actual);
+ // We want to use the multimap's toString() instead of the iterable of entries' toString():
+ this.stringRepresentation = multimapSubject.actual.toString();
+ }
+
+ @Override
+ protected String actualCustomStringRepresentation() {
+ return stringRepresentation;
+ }
+ }
+
+ private class MultimapInOrder implements Ordered {
+ private final Multimap<?, ?> expectedMultimap;
+ private final boolean allowUnexpected;
+
+ MultimapInOrder(boolean allowUnexpected, Multimap<?, ?> expectedMultimap) {
+ this.expectedMultimap = expectedMultimap;
+ this.allowUnexpected = allowUnexpected;
+ }
+
+ /**
+ * Checks whether entries in expected appear in the same order in actual.
+ *
+ * <p>We allow for actual to have more items than the expected to support both {@link
+ * #containsExactly} and {@link #containsAtLeast}.
+ */
+ @Override
+ public void inOrder() {
+ // We use the fact that Sets.intersection's result has the same order as the first parameter
+ boolean keysInOrder =
+ Lists.newArrayList(Sets.intersection(actual.keySet(), expectedMultimap.keySet()))
+ .equals(Lists.newArrayList(expectedMultimap.keySet()));
+
+ LinkedHashSet<Object> keysWithValuesOutOfOrder = Sets.newLinkedHashSet();
+ for (Object key : expectedMultimap.keySet()) {
+ List<?> actualVals = Lists.newArrayList(get(actual, key));
+ List<?> expectedVals = Lists.newArrayList(get(expectedMultimap, key));
+ Iterator<?> actualIterator = actualVals.iterator();
+ for (Object value : expectedVals) {
+ if (!advanceToFind(actualIterator, value)) {
+ keysWithValuesOutOfOrder.add(key);
+ break;
+ }
+ }
+ }
+
+ if (!keysInOrder) {
+ if (!keysWithValuesOutOfOrder.isEmpty()) {
+ failWithActual(
+ simpleFact("contents match, but order was wrong"),
+ simpleFact("keys are not in order"),
+ fact("keys with out-of-order values", keysWithValuesOutOfOrder),
+ simpleFact("---"),
+ fact(
+ allowUnexpected ? "expected to contain at least" : "expected", expectedMultimap));
+ } else {
+ failWithActual(
+ simpleFact("contents match, but order was wrong"),
+ simpleFact("keys are not in order"),
+ simpleFact("---"),
+ fact(
+ allowUnexpected ? "expected to contain at least" : "expected", expectedMultimap));
+ }
+ } else if (!keysWithValuesOutOfOrder.isEmpty()) {
+ failWithActual(
+ simpleFact("contents match, but order was wrong"),
+ fact("keys with out-of-order values", keysWithValuesOutOfOrder),
+ simpleFact("---"),
+ fact(allowUnexpected ? "expected to contain at least" : "expected", expectedMultimap));
+ }
+ }
+ }
+
+ /**
+ * Advances the iterator until it either returns value, or has no more elements.
+ *
+ * <p>Returns true if the value was found, false if the end was reached before finding it.
+ *
+ * <p>This is basically the same as {@link com.google.common.collect.Iterables#contains}, but
+ * where the contract explicitly states that the iterator isn't advanced beyond the value if the
+ * value is found.
+ */
+ private static boolean advanceToFind(Iterator<?> iterator, Object value) {
+ while (iterator.hasNext()) {
+ if (Objects.equal(iterator.next(), value)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static <V> Collection<V> get(Multimap<?, V> multimap, @Nullable Object key) {
+ if (multimap.containsKey(key)) {
+ return multimap.asMap().get(key);
+ } else {
+ return ImmutableList.of();
+ }
+ }
+
+ private static ListMultimap<?, ?> difference(Multimap<?, ?> minuend, Multimap<?, ?> subtrahend) {
+ ListMultimap<Object, Object> difference = LinkedListMultimap.create();
+ for (Object key : minuend.keySet()) {
+ List<?> valDifference =
+ difference(
+ Lists.newArrayList(get(minuend, key)), Lists.newArrayList(get(subtrahend, key)));
+ difference.putAll(key, valDifference);
+ }
+ return difference;
+ }
+
+ private static List<?> difference(List<?> minuend, List<?> subtrahend) {
+ LinkedHashMultiset<Object> remaining = LinkedHashMultiset.<Object>create(subtrahend);
+ List<Object> difference = Lists.newArrayList();
+ for (Object elem : minuend) {
+ if (!remaining.remove(elem)) {
+ difference.add(elem);
+ }
+ }
+ return difference;
+ }
+
+ private static String countDuplicatesMultimap(Multimap<?, ?> multimap) {
+ List<String> entries = new ArrayList<>();
+ for (Object key : multimap.keySet()) {
+ entries.add(key + "=" + SubjectUtils.countDuplicates(get(multimap, key)));
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("{");
+ Joiner.on(", ").appendTo(sb, entries);
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Returns a multimap with all empty strings (as keys or values) replaced by a non-empty human
+ * understandable indicator for an empty string.
+ *
+ * <p>Returns the given multimap if it contains no empty strings.
+ */
+ private static Multimap<?, ?> annotateEmptyStringsMultimap(Multimap<?, ?> multimap) {
+ if (multimap.containsKey("") || multimap.containsValue("")) {
+ ListMultimap<Object, Object> annotatedMultimap = LinkedListMultimap.create();
+ for (Map.Entry<?, ?> entry : multimap.entries()) {
+ Object key = "".equals(entry.getKey()) ? HUMAN_UNDERSTANDABLE_EMPTY_STRING : entry.getKey();
+ Object value =
+ "".equals(entry.getValue()) ? HUMAN_UNDERSTANDABLE_EMPTY_STRING : entry.getValue();
+ annotatedMultimap.put(key, value);
+ }
+ return annotatedMultimap;
+ } else {
+ return multimap;
+ }
+ }
+
+ /**
+ * Starts a method chain for a check in which the actual values (i.e. the values of the {@link
+ * Multimap} under test) are compared to expected values using the given {@link Correspondence}.
+ * The actual values must be of type {@code A}, and the expected values must be of type {@code E}.
+ * The check is actually executed by continuing the method chain. For example:
+ *
+ * <pre>{@code
+ * assertThat(actualMultimap)
+ * .comparingValuesUsing(correspondence)
+ * .containsEntry(expectedKey, expectedValue);
+ * }</pre>
+ *
+ * where {@code actualMultimap} is a {@code Multimap<?, A>} (or, more generally, a {@code
+ * Multimap<?, ? extends A>}), {@code correspondence} is a {@code Correspondence<A, E>}, and
+ * {@code expectedValue} is an {@code E}.
+ *
+ * <p>Note that keys will always be compared with regular object equality ({@link Object#equals}).
+ *
+ * <p>Any of the methods on the returned object may throw {@link ClassCastException} if they
+ * encounter an actual value that is not of type {@code A}.
+ */
+ public <A, E> UsingCorrespondence<A, E> comparingValuesUsing(
+ Correspondence<? super A, ? super E> correspondence) {
+ return new UsingCorrespondence<>(correspondence);
+ }
+
+ // TODO(b/69154276): Add formattingDiffsUsing, like we have on MapSubject, once we have
+ // implemented Smart Diffs for multimaps. We could add it now, but there is no way it could have
+ // any effect, and it would not be testable.
+
+ /**
+ * A partially specified check in which the actual values (i.e. the values of the {@link Multimap}
+ * under test) are compared to expected values using a {@link Correspondence}. The expected values
+ * are of type {@code E}. Call methods on this object to actually execute the check.
+ *
+ * <p>Note that keys will always be compared with regular object equality ({@link Object#equals}).
+ */
+ public final class UsingCorrespondence<A, E> {
+
+ private final Correspondence<? super A, ? super E> correspondence;
+
+ private UsingCorrespondence(Correspondence<? super A, ? super E> correspondence) {
+ this.correspondence = checkNotNull(correspondence);
+ }
+
+ /**
+ * Fails if the multimap does not contain an entry with the given key and a value that
+ * corresponds to the given value.
+ */
+ public void containsEntry(@Nullable Object expectedKey, @Nullable E expectedValue) {
+ if (actual.containsKey(expectedKey)) {
+ // Found matching key.
+ Collection<A> actualValues = getCastActual().asMap().get(expectedKey);
+ Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forMapValues();
+ for (A actualValue : actualValues) {
+ if (correspondence.safeCompare(actualValue, expectedValue, exceptions)) {
+ // Found matching key and value, but we still need to fail if we hit an exception along
+ // the way.
+ if (exceptions.hasCompareException()) {
+ failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .addAll(exceptions.describeAsMainCause())
+ .add(
+ fact(
+ "expected to contain entry",
+ immutableEntry(expectedKey, expectedValue)))
+ .addAll(correspondence.describeForMapValues())
+ .add(
+ fact(
+ "found match (but failing because of exception)",
+ immutableEntry(expectedKey, actualValue)))
+ .add(
+ fact(
+ "full contents",
+ actualCustomStringRepresentationForPackageMembersToCall()))
+ .build());
+ }
+ return;
+ }
+ }
+ // Found matching key with non-matching values.
+ failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .add(fact("expected to contain entry", immutableEntry(expectedKey, expectedValue)))
+ .addAll(correspondence.describeForMapValues())
+ .add(simpleFact("but did not"))
+ .add(fact("though it did contain values for that key", actualValues))
+ .add(
+ fact(
+ "full contents", actualCustomStringRepresentationForPackageMembersToCall()))
+ .addAll(exceptions.describeAsAdditionalInfo())
+ .build());
+ } else {
+ // Did not find matching key.
+ Set<Map.Entry<?, ?>> entries = new LinkedHashSet<>();
+ Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forMapValues();
+ for (Map.Entry<?, A> actualEntry : getCastActual().entries()) {
+ if (correspondence.safeCompare(actualEntry.getValue(), expectedValue, exceptions)) {
+ entries.add(actualEntry);
+ }
+ }
+ if (!entries.isEmpty()) {
+ // Found matching values with non-matching keys.
+ failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .add(
+ fact("expected to contain entry", immutableEntry(expectedKey, expectedValue)))
+ .addAll(correspondence.describeForMapValues())
+ .add(simpleFact("but did not"))
+ // The corresponding failure in the non-Correspondence case reports the keys
+ // mapping to the expected value. Here, we show the full entries, because for some
+ // Correspondences it may not be obvious which of the actual values it was that
+ // corresponded to the expected value.
+ .add(fact("though it did contain entries with matching values", entries))
+ .add(
+ fact(
+ "full contents",
+ actualCustomStringRepresentationForPackageMembersToCall()))
+ .addAll(exceptions.describeAsAdditionalInfo())
+ .build());
+ } else {
+ // Did not find matching key or value.
+ failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .add(
+ fact("expected to contain entry", immutableEntry(expectedKey, expectedValue)))
+ .addAll(correspondence.describeForMapValues())
+ .add(simpleFact("but did not"))
+ .add(
+ fact(
+ "full contents",
+ actualCustomStringRepresentationForPackageMembersToCall()))
+ .addAll(exceptions.describeAsAdditionalInfo())
+ .build());
+ }
+ }
+ }
+
+ /**
+ * Fails if the multimap contains an entry with the given key and a value that corresponds to
+ * the given value.
+ */
+ public void doesNotContainEntry(@Nullable Object excludedKey, @Nullable E excludedValue) {
+ if (actual.containsKey(excludedKey)) {
+ Collection<A> actualValues = getCastActual().asMap().get(excludedKey);
+ List<A> matchingValues = new ArrayList<>();
+ Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forMapValues();
+ for (A actualValue : actualValues) {
+ if (correspondence.safeCompare(actualValue, excludedValue, exceptions)) {
+ matchingValues.add(actualValue);
+ }
+ }
+ // Fail if we found a matching value for the key.
+ if (!matchingValues.isEmpty()) {
+ failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .add(
+ fact(
+ "expected not to contain entry",
+ immutableEntry(excludedKey, excludedValue)))
+ .addAll(correspondence.describeForMapValues())
+ .add(fact("but contained that key with matching values", matchingValues))
+ .add(
+ fact(
+ "full contents",
+ actualCustomStringRepresentationForPackageMembersToCall()))
+ .addAll(exceptions.describeAsAdditionalInfo())
+ .build());
+ } else {
+ // No value matched, but we still need to fail if we hit an exception along the way.
+ if (exceptions.hasCompareException()) {
+ failWithoutActual(
+ ImmutableList.<Fact>builder()
+ .addAll(exceptions.describeAsMainCause())
+ .add(
+ fact(
+ "expected not to contain entry",
+ immutableEntry(excludedKey, excludedValue)))
+ .addAll(correspondence.describeForMapValues())
+ .add(simpleFact("found no match (but failing because of exception)"))
+ .add(
+ fact(
+ "full contents",
+ actualCustomStringRepresentationForPackageMembersToCall()))
+ .build());
+ }
+ }
+ }
+ }
+
+ /**
+ * Fails if the map does not contain exactly the keys in the given multimap, mapping to values
+ * that correspond to the values of the given multimap.
+ *
+ * <p>A subsequent call to {@link Ordered#inOrder} may be made if the caller wishes to verify
+ * that the two Multimaps iterate fully in the same order. That is, their key sets iterate in
+ * the same order, and the corresponding value collections for each key iterate in the same
+ * order.
+ */
+ @CanIgnoreReturnValue
+ public Ordered containsExactlyEntriesIn(Multimap<?, ? extends E> expectedMultimap) {
+ return internalContainsExactlyEntriesIn(expectedMultimap);
+ }
+
+ /*
+ * This helper exists so that we can declare the simpler, type-parameter-free signature for the
+ * public containsExactlyEntriesIn method. This is recommended by Effective Java item 31 (3rd
+ * edition).
+ */
+ private <K, V extends E> Ordered internalContainsExactlyEntriesIn(
+ Multimap<K, V> expectedMultimap) {
+ // Note: The non-fuzzy MultimapSubject.containsExactlyEntriesIn has a custom implementation
+ // and produces somewhat better failure messages simply asserting about the iterables of
+ // entries would: it formats the expected values as k=[v1, v2] rather than k=v1, k=v2; and in
+ // the case where inOrder() fails it says the keys and/or the values for some keys are out of
+ // order. We don't bother with that here. It would be nice, but it would be a lot of added
+ // complexity for little gain.
+ return check()
+ .about(iterableEntries())
+ .that(actual.entries())
+ .comparingElementsUsing(new EntryCorrespondence<K, A, V>(correspondence))
+ .containsExactlyElementsIn(expectedMultimap.entries());
+ }
+
+ /**
+ * Fails if the map does not contain at least the keys in the given multimap, mapping to values
+ * that correspond to the values of the given multimap.
+ *
+ * <p>A subsequent call to {@link Ordered#inOrder} may be made if the caller wishes to verify
+ * that the two Multimaps iterate fully in the same order. That is, their key sets iterate in
+ * the same order, and the corresponding value collections for each key iterate in the same
+ * order.
+ */
+ @CanIgnoreReturnValue
+ public Ordered containsAtLeastEntriesIn(Multimap<?, ? extends E> expectedMultimap) {
+ return internalContainsAtLeastEntriesIn(expectedMultimap);
+ }
+
+ /*
+ * This helper exists so that we can declare the simpler, type-parameter-free signature for the
+ * public containsAtLeastEntriesIn method. This is recommended by Effective Java item 31 (3rd
+ * edition).
+ */
+ private <K, V extends E> Ordered internalContainsAtLeastEntriesIn(
+ Multimap<K, V> expectedMultimap) {
+ // Note: The non-fuzzy MultimapSubject.containsAtLeastEntriesIn has a custom implementation
+ // and produces somewhat better failure messages simply asserting about the iterables of
+ // entries would: it formats the expected values as k=[v1, v2] rather than k=v1, k=v2; and in
+ // the case where inOrder() fails it says the keys and/or the values for some keys are out of
+ // order. We don't bother with that here. It would be nice, but it would be a lot of added
+ // complexity for little gain.
+ return check()
+ .about(iterableEntries())
+ .that(actual.entries())
+ .comparingElementsUsing(new EntryCorrespondence<K, A, V>(correspondence))
+ .containsAtLeastElementsIn(expectedMultimap.entries());
+ }
+
+ /**
+ * Fails if the multimap does not contain exactly the given set of key/value pairs.
+ *
+ * <p><b>Warning:</b> the use of varargs means that we cannot guarantee an equal number of
+ * key/value pairs at compile time. Please make sure you provide varargs in key/value pairs!
+ */
+ @CanIgnoreReturnValue
+ public Ordered containsExactly(
+ @Nullable Object k0, @Nullable E v0, /*@Nullable*/ Object... rest) {
+ @SuppressWarnings("unchecked")
+ Multimap<?, E> expectedMultimap = (Multimap<?, E>) accumulateMultimap(k0, v0, rest);
+ return containsExactlyEntriesIn(expectedMultimap);
+ }
+
+ /** Fails if the multimap is not empty. */
+ @CanIgnoreReturnValue
+ public Ordered containsExactly() {
+ return MultimapSubject.this.containsExactly();
+ }
+
+ /**
+ * Fails if the multimap does not contain at least the given key/value pairs.
+ *
+ * <p><b>Warning:</b> the use of varargs means that we cannot guarantee an equal number of
+ * key/value pairs at compile time. Please make sure you provide varargs in key/value pairs!
+ */
+ @CanIgnoreReturnValue
+ public Ordered containsAtLeast(
+ @Nullable Object k0, @Nullable E v0, /*@Nullable*/ Object... rest) {
+ @SuppressWarnings("unchecked")
+ Multimap<?, E> expectedMultimap = (Multimap<?, E>) accumulateMultimap(k0, v0, rest);
+ return containsAtLeastEntriesIn(expectedMultimap);
+ }
+
+ @SuppressWarnings("unchecked") // throwing ClassCastException is the correct behaviour
+ private Multimap<?, A> getCastActual() {
+ return (Multimap<?, A>) actual;
+ }
+ }
+
+ private static final class EntryCorrespondence<K, A, E>
+ extends Correspondence<Map.Entry<K, A>, Map.Entry<K, E>> {
+
+ private final Correspondence<? super A, ? super E> valueCorrespondence;
+
+ EntryCorrespondence(Correspondence<? super A, ? super E> valueCorrespondence) {
+ this.valueCorrespondence = valueCorrespondence;
+ }
+
+ @Override
+ public boolean compare(Map.Entry<K, A> actual, Map.Entry<K, E> expected) {
+ return Objects.equal(actual.getKey(), expected.getKey())
+ && valueCorrespondence.compare(actual.getValue(), expected.getValue());
+ }
+
+ @Override
+ public String toString() {
+ return lenientFormat(
+ "has a key that is equal to and a value that %s the key and value of",
+ valueCorrespondence);
+ }
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/MultisetSubject.java b/core/src/main/java/com/google/common/truth/MultisetSubject.java
new file mode 100644
index 00000000..40037fe3
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/MultisetSubject.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.Multiset;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Propositions for {@link Multiset} subjects.
+ *
+ * @author Kurt Alfred Kluever
+ */
+public final class MultisetSubject extends IterableSubject {
+
+ private final Multiset<?> actual;
+
+ MultisetSubject(FailureMetadata metadata, @Nullable Multiset<?> multiset) {
+ super(metadata, multiset);
+ this.actual = multiset;
+ }
+
+ /** Fails if the element does not have the given count. */
+ public final void hasCount(@Nullable Object element, int expectedCount) {
+ checkArgument(expectedCount >= 0, "expectedCount(%s) must be >= 0", expectedCount);
+ int actualCount = ((Multiset<?>) actual).count(element);
+ check("count(%s)", element).that(actualCount).isEqualTo(expectedCount);
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/ObjectArraySubject.java b/core/src/main/java/com/google/common/truth/ObjectArraySubject.java
new file mode 100644
index 00000000..cd6b42b5
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/ObjectArraySubject.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import java.util.Arrays;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A Subject for {@code Object[]} and more generically {@code T[]}.
+ *
+ * @author Christian Gruber
+ */
+public final class ObjectArraySubject<T> extends AbstractArraySubject {
+ private final T[] actual;
+
+ ObjectArraySubject(
+ FailureMetadata metadata, @Nullable T /*@Nullable*/[] o, @Nullable String typeDescription) {
+ super(metadata, o, typeDescription);
+ this.actual = o;
+ }
+
+ public IterableSubject asList() {
+ return checkNoNeedToDisplayBothValues("asList()").that(Arrays.asList(actual));
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/Ordered.java b/core/src/main/java/com/google/common/truth/Ordered.java
new file mode 100644
index 00000000..00ed0cc3
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/Ordered.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+/**
+ * Returned by calls like {@link IterableSubject#containsExactly}, {@code Ordered} lets the caller
+ * additionally check that the expected elements were present in the order they were passed to the
+ * previous calls.
+ *
+ * <pre>{@code
+ * assertThat(supportedCharsets).containsExactly("UTF-8", "US-ASCII"); // does not check order
+ * assertThat(supportedCharsets).containsExactly("UTF-8", "US-ASCII").inOrder(); // does check order
+ * }</pre>
+ */
+public interface Ordered {
+
+ /**
+ * An additional assertion, implemented by some containment subjects which allows for a further
+ * constraint of orderedness.
+ */
+ void inOrder();
+}
diff --git a/core/src/main/java/com/google/common/truth/Platform.java b/core/src/main/java/com/google/common/truth/Platform.java
new file mode 100644
index 00000000..698e4bd2
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/Platform.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Throwables.throwIfUnchecked;
+import static com.google.common.truth.DiffUtils.generateUnifiedDiff;
+import static com.google.common.truth.Fact.fact;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.regex.Pattern;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.junit.ComparisonFailure;
+import org.junit.rules.TestRule;
+
+/**
+ * Extracted routines that need to be swapped in for GWT, to allow for minimal deltas between the
+ * GWT and non-GWT version.
+ *
+ * @author Christian Gruber (cgruber@google.com)
+ */
+final class Platform {
+ private Platform() {}
+
+ /** Returns true if the instance is assignable to the type Clazz. */
+ static boolean isInstanceOfType(Object instance, Class<?> clazz) {
+ return clazz.isInstance(instance);
+ }
+
+ /** Determines if the given subject contains a match for the given regex. */
+ static boolean containsMatch(String actual, String regex) {
+ return Pattern.compile(regex).matcher(actual).find();
+ }
+
+ /**
+ * Returns an array containing all of the exceptions that were suppressed to deliver the given
+ * exception. If suppressed exceptions are not supported (pre-Java 1.7), an empty array will be
+ * returned.
+ */
+ static Throwable[] getSuppressed(Throwable throwable) {
+ try {
+ Method getSuppressed = throwable.getClass().getMethod("getSuppressed");
+ return (Throwable[]) getSuppressed.invoke(throwable);
+ } catch (NoSuchMethodException e) {
+ return new Throwable[0];
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ static void cleanStackTrace(Throwable throwable) {
+ StackTraceCleaner.cleanStackTrace(throwable);
+ }
+
+ /**
+ * Tries to infer a name for the root actual value from the bytecode. The "root" actual value is
+ * the value passed to {@code assertThat} or {@code that}, as distinct from any later actual
+ * values produced by chaining calls like {@code hasMessageThat}.
+ */
+ static String inferDescription() {
+ if (isInferDescriptionDisabled()) {
+ return null;
+ }
+
+ AssertionError stack = new AssertionError();
+ /*
+ * cleanStackTrace() lets users turn off cleaning, so it's possible that we'll end up operating
+ * on an uncleaned stack trace. That should be mostly harmless. We could try force-enabling
+ * cleaning for inferDescription() only, but if anyone is turning it off, it might be because of
+ * bugs or confusing stack traces. Force-enabling it here might trigger those same problems.
+ */
+ cleanStackTrace(stack);
+ if (stack.getStackTrace().length == 0) {
+ return null;
+ }
+ StackTraceElement top = stack.getStackTrace()[0];
+ try {
+ /*
+ * Invoke ActualValueInference reflectively so that Truth can be compiled and run without its
+ * dependency, ASM, on the classpath.
+ *
+ * Also, mildly obfuscate the class name that we're looking up. The obfuscation prevents R8
+ * from detecting the usage of ActualValueInference. That in turn lets users exclude it from
+ * the compile-time classpath if they want. (And then *that* probably makes it easier and/or
+ * safer for R8 users (i.e., Android users) to exclude it from the *runtime* classpath. It
+ * would do no good there, anyway, since ASM won't find any .class files to load under
+ * Android. Perhaps R8 will even omit ASM automatically once it detects that it's "unused?")
+ *
+ * TODO(cpovirk): Add a test that runs R8 without ASM present.
+ */
+ String clazz =
+ Joiner.on('.').join("com", "google", "common", "truth", "ActualValueInference");
+ return (String)
+ Class.forName(clazz)
+ .getDeclaredMethod("describeActualValue", String.class, String.class, int.class)
+ .invoke(null, top.getClassName(), top.getMethodName(), top.getLineNumber());
+ } catch (IllegalAccessException
+ | InvocationTargetException
+ | NoSuchMethodException
+ | ClassNotFoundException
+ | LinkageError
+ | RuntimeException e) {
+ // Some possible reasons:
+ // - Inside Google, we omit ActualValueInference entirely under Android.
+ // - Outside Google, someone is running without ASM on the classpath.
+ // - There's a bug.
+ // - We don't handle a new bytecode feature.
+ // TODO(cpovirk): Log a warning, at least for non-ClassNotFoundException, non-LinkageError?
+ return null;
+ }
+ }
+
+ private static final String DIFF_KEY = "diff (-expected +actual)";
+
+ static @Nullable ImmutableList<Fact> makeDiff(String expected, String actual) {
+ ImmutableList<String> expectedLines = splitLines(expected);
+ ImmutableList<String> actualLines = splitLines(actual);
+ List<String> unifiedDiff =
+ generateUnifiedDiff(expectedLines, actualLines, /* contextSize= */ 3);
+ if (unifiedDiff.isEmpty()) {
+ return ImmutableList.of(
+ fact(DIFF_KEY, "(line contents match, but line-break characters differ)"));
+ // TODO(cpovirk): Possibly include the expected/actual value, too?
+ }
+ String result = Joiner.on("\n").join(unifiedDiff);
+ if (result.length() > expected.length() && result.length() > actual.length()) {
+ return null;
+ }
+ return ImmutableList.of(fact(DIFF_KEY, result));
+ }
+
+ private static ImmutableList<String> splitLines(String s) {
+ // splitToList is @Beta, so we avoid it.
+ return ImmutableList.copyOf(Splitter.onPattern("\r?\n").split(s));
+ }
+
+ abstract static class PlatformComparisonFailure extends ComparisonFailure {
+ private final String message;
+
+ /** Separate cause field, in case initCause() fails. */
+ private final @Nullable Throwable cause;
+
+ PlatformComparisonFailure(
+ String message, String expected, String actual, @Nullable Throwable cause) {
+ super(message, expected, actual);
+ this.message = message;
+ this.cause = cause;
+
+ try {
+ initCause(cause);
+ } catch (IllegalStateException alreadyInitializedBecauseOfHarmonyBug) {
+ // See Truth.SimpleAssertionError.
+ }
+ }
+
+ @Override
+ public final String getMessage() {
+ return message;
+ }
+
+ @Override
+ @SuppressWarnings("UnsynchronizedOverridesSynchronized")
+ public final Throwable getCause() {
+ return cause;
+ }
+
+ // To avoid printing the class name before the message.
+ // TODO(cpovirk): Write a test that fails without this. Ditto for SimpleAssertionError.
+ @Override
+ public final String toString() {
+ return getLocalizedMessage();
+ }
+ }
+
+ static String doubleToString(double value) {
+ return Double.toString(value);
+ }
+
+ static String floatToString(float value) {
+ return Float.toString(value);
+ }
+
+ /** Returns a human readable string representation of the throwable's stack trace. */
+ static String getStackTraceAsString(Throwable throwable) {
+ return Throwables.getStackTraceAsString(throwable);
+ }
+
+ /** Tests if current platform is Android. */
+ static boolean isAndroid() {
+ return System.getProperty("java.runtime.name").contains("Android");
+ }
+
+ /**
+ * Wrapping interface of {@link TestRule} to be used within truth.
+ *
+ * <p>Note that the sole purpose of this interface is to allow it to be swapped in GWT
+ * implementation.
+ */
+ interface JUnitTestRule extends TestRule {}
+
+ static final String EXPECT_FAILURE_WARNING_IF_GWT = "";
+
+ // TODO(cpovirk): Share code with StackTraceCleaner?
+ private static boolean isInferDescriptionDisabled() {
+ // Reading system properties might be forbidden.
+ try {
+ return Boolean.parseBoolean(
+ System.getProperty("com.google.common.truth.disable_infer_description"));
+ } catch (SecurityException e) {
+ // Hope for the best.
+ return false;
+ }
+ }
+
+ static AssertionError makeComparisonFailure(
+ ImmutableList<String> messages,
+ ImmutableList<Fact> facts,
+ String expected,
+ String actual,
+ @Nullable Throwable cause) {
+ Class<?> comparisonFailureClass;
+ try {
+ comparisonFailureClass = Class.forName("com.google.common.truth.ComparisonFailureWithFacts");
+ } catch (LinkageError | ClassNotFoundException probablyJunitNotOnClasspath) {
+ /*
+ * LinkageError makes sense, but ClassNotFoundException shouldn't happen:
+ * ComparisonFailureWithFacts should be there, even if its JUnit 4 dependency is not. But it's
+ * harmless to catch an "impossible" exception, and if someone decides to strip the class out
+ * (perhaps along with Platform.PlatformComparisonFailure, to satisfy a tool that is unhappy
+ * because it can't find the latter's superclass because JUnit 4 is also missing?), presumably
+ * we should still fall back to a plain AssertionError.
+ *
+ * TODO(cpovirk): Consider creating and using yet another class like AssertionErrorWithFacts,
+ * not actually extending ComparisonFailure but still exposing getExpected() and getActual()
+ * methods.
+ */
+ return new AssertionErrorWithFacts(messages, facts, cause);
+ }
+ Class<? extends AssertionError> asAssertionErrorSubclass =
+ comparisonFailureClass.asSubclass(AssertionError.class);
+
+ Constructor<? extends AssertionError> constructor;
+ try {
+ constructor =
+ asAssertionErrorSubclass.getDeclaredConstructor(
+ ImmutableList.class,
+ ImmutableList.class,
+ String.class,
+ String.class,
+ Throwable.class);
+ } catch (NoSuchMethodException e) {
+ // That constructor exists.
+ throw newLinkageError(e);
+ }
+
+ try {
+ return constructor.newInstance(messages, facts, expected, actual, cause);
+ } catch (InvocationTargetException e) {
+ throwIfUnchecked(e.getCause());
+ // That constructor has no `throws` clause.
+ throw newLinkageError(e);
+ } catch (InstantiationException e) {
+ // The class is a concrete class.
+ throw newLinkageError(e);
+ } catch (IllegalAccessException e) {
+ // We're accessing a class from within its package.
+ throw newLinkageError(e);
+ }
+ }
+
+ private static LinkageError newLinkageError(Throwable cause) {
+ LinkageError error = new LinkageError(cause.toString());
+ error.initCause(cause);
+ return error;
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/PrimitiveBooleanArraySubject.java b/core/src/main/java/com/google/common/truth/PrimitiveBooleanArraySubject.java
new file mode 100644
index 00000000..47605bc6
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/PrimitiveBooleanArraySubject.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import com.google.common.primitives.Booleans;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A Subject for {@code boolean[]}.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+public final class PrimitiveBooleanArraySubject extends AbstractArraySubject {
+ private final boolean[] actual;
+
+ PrimitiveBooleanArraySubject(
+ FailureMetadata metadata, boolean /*@Nullable*/[] o, @Nullable String typeDescription) {
+ super(metadata, o, typeDescription);
+ this.actual = o;
+ }
+
+ public IterableSubject asList() {
+ return checkNoNeedToDisplayBothValues("asList()").that(Booleans.asList(actual));
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/PrimitiveByteArraySubject.java b/core/src/main/java/com/google/common/truth/PrimitiveByteArraySubject.java
new file mode 100644
index 00000000..59a3c7dd
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/PrimitiveByteArraySubject.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import com.google.common.primitives.Bytes;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A Subject for {@code byte[]}.
+ *
+ * @author Kurt Alfred Kluever
+ */
+public final class PrimitiveByteArraySubject extends AbstractArraySubject {
+ private final byte[] actual;
+
+ PrimitiveByteArraySubject(
+ FailureMetadata metadata, byte /*@Nullable*/[] o, @Nullable String typeDescription) {
+ super(metadata, o, typeDescription);
+ this.actual = o;
+ }
+
+ public IterableSubject asList() {
+ return checkNoNeedToDisplayBothValues("asList()").that(Bytes.asList(actual));
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/PrimitiveCharArraySubject.java b/core/src/main/java/com/google/common/truth/PrimitiveCharArraySubject.java
new file mode 100644
index 00000000..6e94b2cf
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/PrimitiveCharArraySubject.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import com.google.common.primitives.Chars;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A Subject for {@code char[]}.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+public final class PrimitiveCharArraySubject extends AbstractArraySubject {
+ private final char[] actual;
+
+ PrimitiveCharArraySubject(
+ FailureMetadata metadata, char /*@Nullable*/[] o, @Nullable String typeDescription) {
+ super(metadata, o, typeDescription);
+ this.actual = o;
+ }
+
+ public IterableSubject asList() {
+ return checkNoNeedToDisplayBothValues("asList()").that(Chars.asList(actual));
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/PrimitiveDoubleArraySubject.java b/core/src/main/java/com/google/common/truth/PrimitiveDoubleArraySubject.java
new file mode 100644
index 00000000..dc265105
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/PrimitiveDoubleArraySubject.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.truth.Correspondence.tolerance;
+
+import com.google.common.primitives.Doubles;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.util.Arrays;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A Subject for {@code double[]}.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+public final class PrimitiveDoubleArraySubject extends AbstractArraySubject {
+ private final double[] actual;
+
+ PrimitiveDoubleArraySubject(
+ FailureMetadata metadata, double /*@Nullable*/[] o, @Nullable String typeDescription) {
+ super(metadata, o, typeDescription);
+ this.actual = o;
+ }
+
+ /**
+ * A check that the actual array and {@code expected} are arrays of the same length and type,
+ * containing elements such that each element in {@code expected} is equal to each element in the
+ * actual array, and in the same position, with element equality defined the same way that {@link
+ * Arrays#equals(double[], double[])} and {@link Double#equals(Object)} define it (which is
+ * different to the way that the {@code ==} operator on primitive {@code double} defines it). This
+ * method is <i>not</i> recommended when the code under test is doing any kind of arithmetic: use
+ * {@link #usingTolerance} with a suitable tolerance in that case, e.g. {@code
+ * assertThat(actualArray).usingTolerance(1.0e-10).containsExactly(expectedArray).inOrder()}.
+ * (Remember that the exact result of floating point arithmetic is sensitive to apparently trivial
+ * changes such as replacing {@code (a + b) + c} with {@code a + (b + c)}, and that unless {@code
+ * strictfp} is in force even the result of {@code (a + b) + c} is sensitive to the JVM's choice
+ * of precision for the intermediate result.) This method is recommended when the code under test
+ * is specified as either copying values without modification from its input or returning
+ * well-defined literal or constant values.
+ *
+ * <ul>
+ * <li>It considers {@link Double#POSITIVE_INFINITY}, {@link Double#NEGATIVE_INFINITY}, and
+ * {@link Double#NaN} to be equal to themselves (contrast with {@code usingTolerance(0.0)}
+ * which does not).
+ * <li>It does <i>not</i> consider {@code -0.0} to be equal to {@code 0.0} (contrast with {@code
+ * usingTolerance(0.0)} which does).
+ * </ul>
+ */
+ // TODO(cpovirk): Move some or all of this Javadoc to the supertype, maybe deleting this override?
+ @Override
+ public void isEqualTo(Object expected) {
+ super.isEqualTo(expected);
+ }
+
+ /**
+ * A check that the actual array and {@code expected} are not arrays of the same length and type,
+ * containing elements such that each element in {@code expected} is equal to each element in the
+ * actual array, and in the same position, with element equality defined the same way that {@link
+ * Arrays#equals(double[], double[])} and {@link Double#equals(Object)} define it (which is
+ * different to the way that the {@code ==} operator on primitive {@code double} defines it). See
+ * {@link #isEqualTo(Object)} for advice on when exact equality is recommended.
+ *
+ * <ul>
+ * <li>It considers {@link Double#POSITIVE_INFINITY}, {@link Double#NEGATIVE_INFINITY}, and
+ * {@link Double#NaN} to be equal to themselves.
+ * <li>It does <i>not</i> consider {@code -0.0} to be equal to {@code 0.0}.
+ * </ul>
+ */
+ @Override
+ public void isNotEqualTo(Object expected) {
+ super.isNotEqualTo(expected);
+ }
+
+ /**
+ * Starts a method chain for a check in which the actual values (i.e. the elements of the array
+ * under test) are compared to expected elements using a {@link Correspondence} which considers
+ * values to correspond if they are finite values within {@code tolerance} of each other. The
+ * check is actually executed by continuing the method chain. For example:
+ *
+ * <pre>{@code
+ * assertThat(actualDoubleArray).usingTolerance(1.0e-5).contains(3.14159);
+ * }</pre>
+ *
+ * <ul>
+ * <li>It does not consider values to correspond if either value is infinite or NaN.
+ * <li>It considers {@code -0.0} to be within any tolerance of {@code 0.0}.
+ * <li>The expected values provided later in the chain will be {@link Number} instances which
+ * will be converted to doubles, which may result in a loss of precision for some numeric
+ * types.
+ * <li>The subsequent methods in the chain may throw a {@link NullPointerException} if any
+ * expected {@link Number} instance is null.
+ * </ul>
+ *
+ * @param tolerance an inclusive upper bound on the difference between the double values of the
+ * actual and expected numbers, which must be a non-negative finite value, i.e. not {@link
+ * Double#NaN}, {@link Double#POSITIVE_INFINITY}, or negative, including {@code -0.0}
+ */
+ public DoubleArrayAsIterable usingTolerance(double tolerance) {
+ return new DoubleArrayAsIterable(tolerance(tolerance), iterableSubject());
+ }
+
+ private static final Correspondence<Double, Number> EXACT_EQUALITY_CORRESPONDENCE =
+ Correspondence.from(
+ // If we were allowed lambdas, this would be:
+ // (a, e) -> Double.doubleToLongBits(a) == Double.doubleToLongBits(checkedToDouble(e)),
+ new Correspondence.BinaryPredicate<Double, Number>() {
+ @Override
+ public boolean apply(Double actual, Number expected) {
+ return Double.doubleToLongBits(actual)
+ == Double.doubleToLongBits(checkedToDouble(expected));
+ }
+ },
+ "is exactly equal to");
+
+ private static double checkedToDouble(Number expected) {
+ checkNotNull(expected);
+ checkArgument(
+ expected instanceof Double
+ || expected instanceof Float
+ || expected instanceof Integer
+ || expected instanceof Long,
+ "Expected value in assertion using exact double equality was of unsupported type %s "
+ + "(it may not have an exact double representation)",
+ expected.getClass());
+ if (expected instanceof Long) {
+ checkArgument(
+ Math.abs((Long) expected) <= 1L << 53,
+ "Expected value %s in assertion using exact double equality was a long with an absolute "
+ + "value greater than 2^52 which has no exact double representation",
+ expected);
+ }
+ return expected.doubleValue();
+ }
+
+ /**
+ * Starts a method chain for a check in which the actual values (i.e. the elements of the array
+ * under test) are compared to expected elements using a {@link Correspondence} which considers
+ * values to correspond if they are exactly equal, with equality defined by {@link Double#equals}.
+ * This method is <i>not</i> recommended when the code under test is doing any kind of arithmetic:
+ * use {@link #usingTolerance} with a suitable tolerance in that case. (Remember that the exact
+ * result of floating point arithmetic is sensitive to apparently trivial changes such as
+ * replacing {@code (a + b) + c} with {@code a + (b + c)}, and that unless {@code strictfp} is in
+ * force even the result of {@code (a + b) + c} is sensitive to the JVM's choice of precision for
+ * the intermediate result.) This method is recommended when the code under test is specified as
+ * either copying a value without modification from its input or returning a well-defined literal
+ * or constant value. The check is actually executed by continuing the method chain. For example:
+ *
+ * <pre>{@code
+ * assertThat(actualDoubleArray).usingExactEquality().contains(3.14159);
+ * }</pre>
+ *
+ * <p>For convenience, some subsequent methods accept expected values as {@link Number} instances.
+ * These numbers must be either of type {@link Double}, {@link Float}, {@link Integer}, or {@link
+ * Long}, and if they are {@link Long} then their absolute values must not exceed 2^53 which is
+ * just over 9e15. (This restriction ensures that the expected values have exact {@link Double}
+ * representations: using exact equality makes no sense if they do not.)
+ *
+ * <ul>
+ * <li>It considers {@link Double#POSITIVE_INFINITY}, {@link Double#NEGATIVE_INFINITY}, and
+ * {@link Double#NaN} to be equal to themselves (contrast with {@code usingTolerance(0.0)}
+ * which does not).
+ * <li>It does <i>not</i> consider {@code -0.0} to be equal to {@code 0.0} (contrast with {@code
+ * usingTolerance(0.0)} which does).
+ * <li>The subsequent methods in the chain may throw a {@link NullPointerException} if any
+ * expected {@link Double} instance is null.
+ * </ul>
+ */
+ public DoubleArrayAsIterable usingExactEquality() {
+ return new DoubleArrayAsIterable(EXACT_EQUALITY_CORRESPONDENCE, iterableSubject());
+ }
+
+ /**
+ * A partially specified check for doing assertions on the array similar to the assertions
+ * supported for {@link Iterable} subjects, in which the elements of the array under test are
+ * compared to expected elements using either exact or tolerant double equality: see {@link
+ * #usingExactEquality} and {@link #usingTolerance}. Call methods on this object to actually
+ * execute the check.
+ *
+ * <p>In the exact equality case, the methods on this class which take {@link Number} arguments
+ * only accept certain instances: again, see {@link #usingExactEquality} for details.
+ */
+ public static final class DoubleArrayAsIterable
+ extends IterableSubject.UsingCorrespondence<Double, Number> {
+
+ DoubleArrayAsIterable(
+ Correspondence<? super Double, Number> correspondence, IterableSubject subject) {
+ super(subject, correspondence);
+ }
+
+ /**
+ * As {@link #containsAtLeast(Object, Object, Object...)} but taking a primitive double array.
+ */
+ @CanIgnoreReturnValue
+ public Ordered containsAtLeast(double[] expected) {
+ return containsAtLeastElementsIn(Doubles.asList(expected));
+ }
+
+ /** As {@link #containsAnyOf(Object, Object, Object...)} but taking a primitive double array. */
+ public void containsAnyOf(double[] expected) {
+ containsAnyIn(Doubles.asList(expected));
+ }
+
+ /** As {@link #containsExactly(Object...)} but taking a primitive double array. */
+ @CanIgnoreReturnValue
+ public Ordered containsExactly(double[] expected) {
+ return containsExactlyElementsIn(Doubles.asList(expected));
+ }
+
+ /**
+ * As {@link #containsNoneOf(Object, Object, Object...)} but taking a primitive double array.
+ */
+ public void containsNoneOf(double[] excluded) {
+ containsNoneIn(Doubles.asList(excluded));
+ }
+ }
+
+ private IterableSubject iterableSubject() {
+ return checkNoNeedToDisplayBothValues("asList()")
+ .about(iterablesWithCustomDoubleToString())
+ .that(Doubles.asList(actual));
+ }
+
+ /*
+ * TODO(cpovirk): Should we make Doubles.asList().toString() smarter rather than do all this?
+ *
+ * TODO(cpovirk): Or find a general solution for this and MultimapSubject.IterableEntries. But
+ * note that here we don't use _exactly_ PrimitiveDoubleArraySubject.this.toString(), as that
+ * contains "double[]." Or maybe we should stop including that in
+ * PrimitiveDoubleArraySubject.this.toString(), too, someday?
+ */
+ private Factory<IterableSubject, Iterable<?>> iterablesWithCustomDoubleToString() {
+ return new Factory<IterableSubject, Iterable<?>>() {
+ @Override
+ public IterableSubject createSubject(FailureMetadata metadata, Iterable<?> actual) {
+ return new IterableSubjectWithInheritedToString(metadata, actual);
+ }
+ };
+ }
+
+ private final class IterableSubjectWithInheritedToString extends IterableSubject {
+
+ IterableSubjectWithInheritedToString(FailureMetadata metadata, Iterable<?> actual) {
+ super(metadata, actual);
+ }
+
+ @Override
+ protected String actualCustomStringRepresentation() {
+ return PrimitiveDoubleArraySubject.this
+ .actualCustomStringRepresentationForPackageMembersToCall();
+ }
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/PrimitiveFloatArraySubject.java b/core/src/main/java/com/google/common/truth/PrimitiveFloatArraySubject.java
new file mode 100644
index 00000000..ae106d5f
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/PrimitiveFloatArraySubject.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.truth.Correspondence.tolerance;
+
+import com.google.common.primitives.Floats;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.util.Arrays;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A Subject for {@code float[]}.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+public final class PrimitiveFloatArraySubject extends AbstractArraySubject {
+ private final float[] actual;
+
+ PrimitiveFloatArraySubject(
+ FailureMetadata metadata, float /*@Nullable*/[] o, @Nullable String typeDescription) {
+ super(metadata, o, typeDescription);
+ this.actual = o;
+ }
+
+ /**
+ * A check that the actual array and {@code expected} are arrays of the same length and type,
+ * containing elements such that each element in {@code expected} is equal to each element in the
+ * actual array, and in the same position, with element equality defined the same way that {@link
+ * Arrays#equals(float[], float[])} and {@link Float#equals(Object)} define it (which is different
+ * to the way that the {@code ==} operator on primitive {@code float} defines it). This method is
+ * <i>not</i> recommended when the code under test is doing any kind of arithmetic: use {@link
+ * #usingTolerance} with a suitable tolerance in that case, e.g. {@code
+ * assertThat(actualArray).usingTolerance(1.0e-5).containsExactly(expectedArray).inOrder()}.
+ * (Remember that the exact result of floating point arithmetic is sensitive to apparently trivial
+ * changes such as replacing {@code (a + b) + c} with {@code a + (b + c)}, and that unless {@code
+ * strictfp} is in force even the result of {@code (a + b) + c} is sensitive to the JVM's choice
+ * of precision for the intermediate result.) This method is recommended when the code under test
+ * is specified as either copying values without modification from its input or returning
+ * well-defined literal or constant values.
+ *
+ * <ul>
+ * <li>It considers {@link Float#POSITIVE_INFINITY}, {@link Float#NEGATIVE_INFINITY}, and {@link
+ * Float#NaN} to be equal to themselves (contrast with {@code usingTolerance(0.0)} which
+ * does not).
+ * <li>It does <i>not</i> consider {@code -0.0f} to be equal to {@code 0.0f} (contrast with
+ * {@code usingTolerance(0.0)} which does).
+ * </ul>
+ */
+ @Override
+ public void isEqualTo(Object expected) {
+ super.isEqualTo(expected);
+ }
+
+ /**
+ * A check that the actual array and {@code expected} are not arrays of the same length and type,
+ * containing elements such that each element in {@code expected} is equal to each element in the
+ * actual array, and in the same position, with element equality defined the same way that {@link
+ * Arrays#equals(float[], float[])} and {@link Float#equals(Object)} define it (which is different
+ * to the way that the {@code ==} operator on primitive {@code float} defines it). See {@link
+ * #isEqualTo(Object)} for advice on when exact equality is recommended.
+ *
+ * <ul>
+ * <li>It considers {@link Float#POSITIVE_INFINITY}, {@link Float#NEGATIVE_INFINITY}, and {@link
+ * Float#NaN} to be equal to themselves.
+ * <li>It does <i>not</i> consider {@code -0.0} to be equal to {@code 0.0}.
+ * </ul>
+ */
+ @Override
+ public void isNotEqualTo(Object expected) {
+ super.isNotEqualTo(expected);
+ }
+
+ /**
+ * Starts a method chain for a check in which the actual values (i.e. the elements of the array
+ * under test) are compared to expected elements using a {@link Correspondence} which considers
+ * values to correspond if they are finite values within {@code tolerance} of each other. The
+ * check is actually executed by continuing the method chain. For example:
+ *
+ * <pre>{@code
+ * assertThat(actualFloatArray).usingTolerance(1.0e-5f).contains(3.14159f);
+ * }</pre>
+ *
+ * <ul>
+ * <li>It does not consider values to correspond if either value is infinite or NaN.
+ * <li>It considers {@code -0.0f} to be within any tolerance of {@code 0.0f}.
+ * <li>The expected values provided later in the chain will be {@link Number} instances which
+ * will be converted to floats, which may result in a loss of precision for some numeric
+ * types.
+ * <li>The subsequent methods in the chain may throw a {@link NullPointerException} if any
+ * expected {@link Number} instance is null.
+ * </ul>
+ *
+ * @param tolerance an inclusive upper bound on the difference between the float values of the
+ * actual and expected numbers, which must be a non-negative finite value, i.e. not {@link
+ * Float#NaN}, {@link Float#POSITIVE_INFINITY}, or negative, including {@code -0.0f}
+ */
+ public FloatArrayAsIterable usingTolerance(double tolerance) {
+ return new FloatArrayAsIterable(tolerance(tolerance), iterableSubject());
+ }
+
+ private static final Correspondence<Float, Number> EXACT_EQUALITY_CORRESPONDENCE =
+ Correspondence.from(
+ // If we were allowed lambdas, this would be:
+ // (a, e) -> Float.floatToIntBits(a) == Float.floatToIntBits(checkedToFloat(e)),
+ new Correspondence.BinaryPredicate<Float, Number>() {
+
+ @Override
+ public boolean apply(Float actual, Number expected) {
+ return Float.floatToIntBits(actual) == Float.floatToIntBits(checkedToFloat(expected));
+ }
+ },
+ "is exactly equal to");
+
+ private static float checkedToFloat(Number expected) {
+ checkNotNull(expected);
+ checkArgument(
+ !(expected instanceof Double),
+ "Expected value in assertion using exact float equality was a double, which is not "
+ + "supported as a double may not have an exact float representation");
+ checkArgument(
+ expected instanceof Float || expected instanceof Integer || expected instanceof Long,
+ "Expected value in assertion using exact float equality was of unsupported type %s "
+ + "(it may not have an exact float representation)",
+ expected.getClass());
+ if (expected instanceof Integer) {
+ checkArgument(
+ Math.abs((Integer) expected) <= 1 << 24,
+ "Expected value %s in assertion using exact float equality was an int with an absolute "
+ + "value greater than 2^24 which has no exact float representation",
+ expected);
+ }
+ if (expected instanceof Long) {
+ checkArgument(
+ Math.abs((Long) expected) <= 1L << 24,
+ "Expected value %s in assertion using exact float equality was a long with an absolute "
+ + "value greater than 2^24 which has no exact float representation",
+ expected);
+ }
+ return expected.floatValue();
+ }
+
+ /**
+ * Starts a method chain for a check in which the actual values (i.e. the elements of the array
+ * under test) are compared to expected elements using a {@link Correspondence} which considers
+ * values to correspond if they are exactly equal, with equality defined by {@link Float#equals}.
+ * This method is <i>not</i> recommended when the code under test is doing any kind of arithmetic:
+ * use {@link #usingTolerance} with a suitable tolerance in that case. (Remember that the exact
+ * result of floating point arithmetic is sensitive to apparently trivial changes such as
+ * replacing {@code (a + b) + c} with {@code a + (b + c)}, and that unless {@code strictfp} is in
+ * force even the result of {@code (a + b) + c} is sensitive to the JVM's choice of precision for
+ * the intermediate result.) This method is recommended when the code under test is specified as
+ * either copying a value without modification from its input or returning a well-defined literal
+ * or constant value. The check is actually executed by continuing the method chain. For example:
+ *
+ * <pre>{@code
+ * assertThat(actualFloatArray).usingExactEquality().contains(3.14159f);
+ * }</pre>
+ *
+ * <p>For convenience, some subsequent methods accept expected values as {@link Number} instances.
+ * These numbers must be either of type {@link Float}, {@link Integer}, or {@link Long}, and if
+ * they are {@link Integer} or {@link Long} then their absolute values must not exceed 2^24 which
+ * is 16,777,216. (This restriction ensures that the expected values have exact {@link Float}
+ * representations: using exact equality makes no sense if they do not.)
+ *
+ * <ul>
+ * <li>It considers {@link Float#POSITIVE_INFINITY}, {@link Float#NEGATIVE_INFINITY}, and {@link
+ * Float#NaN} to be equal to themselves (contrast with {@code usingTolerance(0.0)} which
+ * does not).
+ * <li>It does <i>not</i> consider {@code -0.0f} to be equal to {@code 0.0f} (contrast with
+ * {@code usingTolerance(0.0)} which does).
+ * <li>The subsequent methods in the chain may throw a {@link NullPointerException} if any
+ * expected {@link Float} instance is null.
+ * </ul>
+ */
+ public FloatArrayAsIterable usingExactEquality() {
+ return new FloatArrayAsIterable(EXACT_EQUALITY_CORRESPONDENCE, iterableSubject());
+ }
+
+ /**
+ * A partially specified check for doing assertions on the array similar to the assertions
+ * supported for {@link Iterable} subjects, in which the elements of the array under test are
+ * compared to expected elements using either exact or tolerant float equality: see {@link
+ * #usingExactEquality} and {@link #usingTolerance}. Call methods on this object to actually
+ * execute the check.
+ *
+ * <p>In the exact equality case, the methods on this class which take {@link Number} arguments
+ * only accept certain instances: again, see {@link #usingExactEquality} for details.
+ */
+ public static final class FloatArrayAsIterable
+ extends IterableSubject.UsingCorrespondence<Float, Number> {
+
+ FloatArrayAsIterable(
+ Correspondence<? super Float, Number> correspondence, IterableSubject subject) {
+ super(subject, correspondence);
+ }
+
+ /**
+ * As {@link #containsAtLeast(Object, Object, Object...)} but taking a primitive float array.
+ */
+ @CanIgnoreReturnValue
+ public Ordered containsAtLeast(float[] expected) {
+ return containsAtLeastElementsIn(Floats.asList(expected));
+ }
+
+ /** As {@link #containsAnyOf(Object, Object, Object...)} but taking a primitive float array. */
+ public void containsAnyOf(float[] expected) {
+ containsAnyIn(Floats.asList(expected));
+ }
+
+ /** As {@link #containsExactly(Object...)} but taking a primitive float array. */
+ @CanIgnoreReturnValue
+ public Ordered containsExactly(float[] expected) {
+ return containsExactlyElementsIn(Floats.asList(expected));
+ }
+
+ /** As {@link #containsNoneOf(Object, Object, Object...)} but taking a primitive float array. */
+ public void containsNoneOf(float[] excluded) {
+ containsNoneIn(Floats.asList(excluded));
+ }
+ }
+
+ private IterableSubject iterableSubject() {
+ return checkNoNeedToDisplayBothValues("asList()")
+ .about(iterablesWithCustomFloatToString())
+ .that(Floats.asList(actual));
+ }
+
+ /*
+ * TODO(cpovirk): Should we make Floats.asList().toString() smarter rather than do all this?
+ *
+ * TODO(cpovirk): Or find a general solution for this and MultimapSubject.IterableEntries. But
+ * note that here we don't use _exactly_ PrimitiveFloatArraySubject.this.toString(), as that
+ * contains "float[]." Or maybe we should stop including that in
+ * PrimitiveFloatArraySubject.this.toString(), too, someday?
+ */
+ private Factory<IterableSubject, Iterable<?>> iterablesWithCustomFloatToString() {
+ return new Factory<IterableSubject, Iterable<?>>() {
+ @Override
+ public IterableSubject createSubject(FailureMetadata metadata, Iterable<?> actual) {
+ return new IterableSubjectWithInheritedToString(metadata, actual);
+ }
+ };
+ }
+
+ private final class IterableSubjectWithInheritedToString extends IterableSubject {
+
+ IterableSubjectWithInheritedToString(FailureMetadata metadata, Iterable<?> actual) {
+ super(metadata, actual);
+ }
+
+ @Override
+ protected String actualCustomStringRepresentation() {
+ return PrimitiveFloatArraySubject.this
+ .actualCustomStringRepresentationForPackageMembersToCall();
+ }
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/PrimitiveIntArraySubject.java b/core/src/main/java/com/google/common/truth/PrimitiveIntArraySubject.java
new file mode 100644
index 00000000..f2c4abb5
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/PrimitiveIntArraySubject.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import com.google.common.primitives.Ints;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A Subject for {@code int[]}.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+public final class PrimitiveIntArraySubject extends AbstractArraySubject {
+ private final int[] actual;
+
+ PrimitiveIntArraySubject(
+ FailureMetadata metadata, int /*@Nullable*/[] o, @Nullable String typeDescription) {
+ super(metadata, o, typeDescription);
+ this.actual = o;
+ }
+
+ public IterableSubject asList() {
+ return checkNoNeedToDisplayBothValues("asList()").that(Ints.asList(actual));
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/PrimitiveLongArraySubject.java b/core/src/main/java/com/google/common/truth/PrimitiveLongArraySubject.java
new file mode 100644
index 00000000..1d5aec0c
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/PrimitiveLongArraySubject.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import com.google.common.primitives.Longs;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A Subject for {@code long[]}.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+public final class PrimitiveLongArraySubject extends AbstractArraySubject {
+ private final long[] actual;
+
+ PrimitiveLongArraySubject(
+ FailureMetadata metadata, long /*@Nullable*/[] o, @Nullable String typeDescription) {
+ super(metadata, o, typeDescription);
+ this.actual = o;
+ }
+
+ public IterableSubject asList() {
+ return checkNoNeedToDisplayBothValues("asList()").that(Longs.asList(actual));
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/PrimitiveShortArraySubject.java b/core/src/main/java/com/google/common/truth/PrimitiveShortArraySubject.java
new file mode 100644
index 00000000..cd17c874
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/PrimitiveShortArraySubject.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2015 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import com.google.common.primitives.Shorts;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A Subject for {@code short[]}.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+public final class PrimitiveShortArraySubject extends AbstractArraySubject {
+ private final short[] actual;
+
+ PrimitiveShortArraySubject(
+ FailureMetadata metadata, short /*@Nullable*/[] o, @Nullable String typeDescription) {
+ super(metadata, o, typeDescription);
+ this.actual = o;
+ }
+
+ public IterableSubject asList() {
+ return checkNoNeedToDisplayBothValues("asList()").that(Shorts.asList(actual));
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/SimpleSubjectBuilder.java b/core/src/main/java/com/google/common/truth/SimpleSubjectBuilder.java
new file mode 100644
index 00000000..4451ee46
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/SimpleSubjectBuilder.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * In a fluent assertion chain, exposes the most common {@code that} method, which accepts a value
+ * under test and returns a {@link Subject}.
+ *
+ * <p>For more information about the methods in this class, see <a
+ * href="https://truth.dev/faq#full-chain">this FAQ entry</a>.
+ *
+ * <h3>For people extending Truth</h3>
+ *
+ * <p>You won't extend this type. When you write a custom subject, see <a
+ * href="https://truth.dev/extension">our doc on extensions</a>.
+ */
+public final class SimpleSubjectBuilder<SubjectT extends Subject, ActualT> {
+ private final FailureMetadata metadata;
+ private final Subject.Factory<SubjectT, ActualT> subjectFactory;
+
+ SimpleSubjectBuilder(
+ FailureMetadata metadata, Subject.Factory<SubjectT, ActualT> subjectFactory) {
+ this.metadata = checkNotNull(metadata);
+ this.subjectFactory = checkNotNull(subjectFactory);
+ }
+
+ public SubjectT that(@Nullable ActualT actual) {
+ return subjectFactory.createSubject(metadata, actual);
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/StackTraceCleaner.java b/core/src/main/java/com/google/common/truth/StackTraceCleaner.java
new file mode 100644
index 00000000..04ac3980
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/StackTraceCleaner.java
@@ -0,0 +1,436 @@
+/*
+ * Copyright (c) 2017 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static java.lang.Thread.currentThread;
+
+import com.google.common.annotations.GwtIncompatible;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+
+/** Utility that cleans stack traces to remove noise from common frameworks. */
+@GwtIncompatible
+final class StackTraceCleaner {
+
+ static final String CLEANER_LINK = "https://goo.gl/aH3UyP";
+
+ /**
+ * <b>Call {@link Platform#cleanStackTrace} rather than calling this directly.</b>
+ *
+ * <p>Cleans the stack trace on the given {@link Throwable}, replacing the original stack trace
+ * stored on the instance (see {@link Throwable#setStackTrace(StackTraceElement[])}).
+ *
+ * <p>Removes Truth stack frames from the top and JUnit framework and reflective call frames from
+ * the bottom. Collapses the frames for various frameworks in the middle of the trace as well.
+ */
+ static void cleanStackTrace(Throwable throwable) {
+ new StackTraceCleaner(throwable).clean(Sets.<Throwable>newIdentityHashSet());
+ }
+
+ private final Throwable throwable;
+ private final List<StackTraceElementWrapper> cleanedStackTrace = new ArrayList<>();
+ private StackTraceElementWrapper lastStackFrameElementWrapper = null;
+ private StackFrameType currentStreakType = null;
+ private int currentStreakLength = 0;
+
+ /**
+ * A new instance is instantiated for each throwable to be cleaned. This is so that helper methods
+ * can make use of instance variables describing the state of the cleaning process.
+ */
+ private StackTraceCleaner(Throwable throwable) {
+ this.throwable = throwable;
+ }
+
+ // TODO(b/135924708): Add this to the test runners so that we clean all stack traces, not just
+ // those of exceptions originating in Truth.
+ /** Cleans the stack trace on {@code throwable}, replacing the trace that was originally on it. */
+ private void clean(Set<Throwable> seenThrowables) {
+ // Stack trace cleaning can be disabled using a system property.
+ if (isStackTraceCleaningDisabled()) {
+ return;
+ }
+
+ /*
+ * TODO(cpovirk): Consider wrapping this whole method in a try-catch in case there are any bugs.
+ * It would be a shame for us to fail to report the "real" assertion failure because we're
+ * instead reporting a bug in Truth's cosmetic stack cleaning.
+ */
+
+ // Prevent infinite recursion if there is a reference cycle between Throwables.
+ if (seenThrowables.contains(throwable)) {
+ return;
+ }
+ seenThrowables.add(throwable);
+
+ StackTraceElement[] stackFrames = throwable.getStackTrace();
+
+ int stackIndex = stackFrames.length - 1;
+ for (; stackIndex >= 0 && !isTruthEntrance(stackFrames[stackIndex]); stackIndex--) {
+ // Find first frame that enters Truth's world, and remove all above.
+ }
+ stackIndex += 1;
+
+ int endIndex = 0;
+ for (;
+ endIndex < stackFrames.length && !isJUnitIntrastructure(stackFrames[endIndex]);
+ endIndex++) {
+ // Find last frame of setup frames, and remove from there down.
+ }
+ /*
+ * If the stack trace would be empty, the error was probably thrown from "JUnit infrastructure"
+ * frames. Keep those frames around (though much of JUnit itself and related startup frames will
+ * still be removed by the remainder of this method) so that the user sees a useful stack.
+ */
+ if (!(stackIndex < endIndex)) {
+ endIndex = stackFrames.length;
+ }
+
+ for (; stackIndex < endIndex; stackIndex++) {
+ StackTraceElementWrapper stackTraceElementWrapper =
+ new StackTraceElementWrapper(stackFrames[stackIndex]);
+ // Always keep frames that might be useful.
+ if (stackTraceElementWrapper.getStackFrameType() == StackFrameType.NEVER_REMOVE) {
+ endStreak();
+ cleanedStackTrace.add(stackTraceElementWrapper);
+ continue;
+ }
+
+ // Otherwise, process the current frame for collapsing
+ addToStreak(stackTraceElementWrapper);
+
+ lastStackFrameElementWrapper = stackTraceElementWrapper;
+ }
+
+ // Close out the streak on the bottom of the stack.
+ endStreak();
+
+ // Filter out testing framework and reflective calls from the bottom of the stack
+ ListIterator<StackTraceElementWrapper> iterator =
+ cleanedStackTrace.listIterator(cleanedStackTrace.size());
+ while (iterator.hasPrevious()) {
+ StackTraceElementWrapper stackTraceElementWrapper = iterator.previous();
+ if (stackTraceElementWrapper.getStackFrameType() == StackFrameType.TEST_FRAMEWORK
+ || stackTraceElementWrapper.getStackFrameType() == StackFrameType.REFLECTION) {
+ iterator.remove();
+ } else {
+ break;
+ }
+ }
+
+ // Replace the stack trace on the Throwable with the cleaned one.
+ StackTraceElement[] result = new StackTraceElement[cleanedStackTrace.size()];
+ for (int i = 0; i < result.length; i++) {
+ result[i] = cleanedStackTrace.get(i).getStackTraceElement();
+ }
+ throwable.setStackTrace(result);
+
+ // Recurse on any related Throwables that are attached to this one
+ if (throwable.getCause() != null) {
+ new StackTraceCleaner(throwable.getCause()).clean(seenThrowables);
+ }
+ for (Throwable suppressed : Platform.getSuppressed(throwable)) {
+ new StackTraceCleaner(suppressed).clean(seenThrowables);
+ }
+ }
+
+ /**
+ * Either adds the given frame to the running streak or closes out the running streak and starts a
+ * new one.
+ */
+ private void addToStreak(StackTraceElementWrapper stackTraceElementWrapper) {
+ if (stackTraceElementWrapper.getStackFrameType() != currentStreakType) {
+ endStreak();
+ currentStreakType = stackTraceElementWrapper.getStackFrameType();
+ currentStreakLength = 1;
+ } else {
+ currentStreakLength++;
+ }
+ }
+
+ /** Ends the current streak, adding a summary frame to the result. Resets the streak counter. */
+ private void endStreak() {
+ if (currentStreakLength == 0) {
+ return;
+ }
+
+ if (currentStreakLength == 1) {
+ // A single frame isn't a streak. Just include the frame as-is in the result.
+ cleanedStackTrace.add(lastStackFrameElementWrapper);
+ } else {
+ // Add a single frame to the result summarizing the streak of framework frames
+ cleanedStackTrace.add(createStreakReplacementFrame(currentStreakType, currentStreakLength));
+ }
+
+ clearStreak();
+ }
+
+ /** Resets the streak counter. */
+ private void clearStreak() {
+ currentStreakType = null;
+ currentStreakLength = 0;
+ }
+
+ private static final ImmutableSet<String> SUBJECT_CLASS =
+ ImmutableSet.of(Subject.class.getCanonicalName());
+
+ private static final ImmutableSet<String> STANDARD_SUBJECT_BUILDER_CLASS =
+ ImmutableSet.of(StandardSubjectBuilder.class.getCanonicalName());
+
+ private static boolean isTruthEntrance(StackTraceElement stackTraceElement) {
+ return isFromClassOrClassNestedInside(stackTraceElement, SUBJECT_CLASS)
+ /*
+ * Don't match classes _nested inside_ StandardSubjectBuilder because that would match
+ * Expect's Statement implementation. While we want to strip everything from there _down_, we
+ * don't want to strip everything from there _up_ (which would strip the test class itself!).
+ *
+ * (StandardSubjectBuilder is listed here only for its fail() methods, anyway, so we don't
+ * have to worry about nested classes like we do with Subject.)
+ */
+ || isFromClassDirectly(stackTraceElement, STANDARD_SUBJECT_BUILDER_CLASS);
+ }
+
+ private static final ImmutableSet<String> JUNIT_INFRASTRUCTURE_CLASSES =
+ ImmutableSet.of("org.junit.runner.Runner", "org.junit.runners.model.Statement");
+
+ private static boolean isJUnitIntrastructure(StackTraceElement stackTraceElement) {
+ // It's not clear whether looking at nested classes here is useful, harmful, or neutral.
+ return isFromClassOrClassNestedInside(stackTraceElement, JUNIT_INFRASTRUCTURE_CLASSES);
+ }
+
+ private static boolean isFromClassOrClassNestedInside(
+ StackTraceElement stackTraceElement, ImmutableSet<String> recognizedClasses) {
+ Class<?> stackClass;
+ try {
+ stackClass = loadClass(stackTraceElement.getClassName());
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ try {
+ for (; stackClass != null; stackClass = stackClass.getEnclosingClass()) {
+ for (String recognizedClass : recognizedClasses) {
+ if (isSubtypeOf(stackClass, recognizedClass)) {
+ return true;
+ }
+ }
+ }
+ } catch (Error e) {
+ if (e.getClass().getName().equals("com.google.j2objc.ReflectionStrippedError")) {
+ /*
+ * We're running under j2objc without reflection. Skip testing the enclosing classes. At
+ * least we tested the class itself against all the recognized classes.
+ *
+ * TODO(cpovirk): The smarter thing might be to guess the name of the enclosing classes by
+ * removing "$Foo" from the end of the name. But this should be good enough for now.
+ */
+ return false;
+ }
+ if (e instanceof IncompatibleClassChangeError) {
+ // OEM class-loading bug? https://issuetracker.google.com/issues/37045084
+ return false;
+ }
+ throw e;
+ }
+ return false;
+ }
+
+ private static boolean isSubtypeOf(Class<?> subclass, String superclass) {
+ for (; subclass != null; subclass = subclass.getSuperclass()) {
+ if (subclass.getCanonicalName() != null && subclass.getCanonicalName().equals(superclass)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isFromClassDirectly(
+ StackTraceElement stackTraceElement, ImmutableSet<String> recognizedClasses) {
+ Class<?> stackClass;
+ try {
+ stackClass = loadClass(stackTraceElement.getClassName());
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ for (String recognizedClass : recognizedClasses) {
+ if (isSubtypeOf(stackClass, recognizedClass)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Using plain Class.forName can cause problems.
+ /*
+ * TODO(cpovirk): Consider avoiding classloading entirely by reading classes with ASM. But that
+ * won't work on Android, so we might ultimately need classloading as a fallback. Another
+ * possibility is to load classes in a fresh, isolated classloader. However, that requires
+ * creating a list of jars to load from, which is fragile and would also require special handling
+ * under Android. If we're lucky, this new solution will just work: The classes should already be
+ * loaded, anyway, since they appear on the stack, so we just have to hope that we have the right
+ * classloader.
+ */
+ private static Class<?> loadClass(String name) throws ClassNotFoundException {
+ ClassLoader loader =
+ firstNonNull(
+ currentThread().getContextClassLoader(), StackTraceCleaner.class.getClassLoader());
+ return loader.loadClass(name);
+ }
+
+ /**
+ * Wrapper around a {@link StackTraceElement} for calculating and holding the metadata used to
+ * clean the stack trace.
+ */
+ private static class StackTraceElementWrapper {
+
+ private final StackTraceElement stackTraceElement;
+ private final StackFrameType stackFrameType;
+
+ /** Creates a wrapper with the given frame with frame type inferred from frame's class name. */
+ StackTraceElementWrapper(StackTraceElement stackTraceElement) {
+ this(stackTraceElement, StackFrameType.forClassName(stackTraceElement.getClassName()));
+ }
+
+ /** Creates a wrapper with the given frame and the given frame type. */
+ StackTraceElementWrapper(StackTraceElement stackTraceElement, StackFrameType stackFrameType) {
+ this.stackTraceElement = stackTraceElement;
+ this.stackFrameType = stackFrameType;
+ }
+
+ /** Returns the type of this frame. */
+ StackFrameType getStackFrameType() {
+ return stackFrameType;
+ }
+
+ /** Returns the wrapped {@link StackTraceElement}. */
+ StackTraceElement getStackTraceElement() {
+ return stackTraceElement;
+ }
+ }
+
+ private static StackTraceElementWrapper createStreakReplacementFrame(
+ StackFrameType stackFrameType, int length) {
+ return new StackTraceElementWrapper(
+ new StackTraceElement(
+ "[["
+ + stackFrameType.getName()
+ + ": "
+ + length
+ + " frames collapsed ("
+ + CLEANER_LINK
+ + ")]]",
+ "",
+ "",
+ 0),
+ stackFrameType);
+ }
+
+ /**
+ * Enum of the package or class-name based categories of stack frames that might be removed or
+ * collapsed by the cleaner.
+ */
+ private enum StackFrameType {
+ NEVER_REMOVE("N/A"),
+ TEST_FRAMEWORK(
+ "Testing framework",
+ "junit",
+ "org.junit",
+ "com.google.testing.junit",
+ "com.google.testing.testsize",
+ "com.google.testing.util"),
+ REFLECTION("Reflective call", "java.lang.reflect", "jdk.internal.reflect", "sun.reflect"),
+ CONCURRENT_FRAMEWORK(
+ "Concurrent framework",
+ "com.google.tracing.CurrentContext",
+ "com.google.common.util.concurrent",
+ "java.util.concurrent.ForkJoin");
+
+ /** Helper method to determine the frame type from the fully qualified class name. */
+ private static StackFrameType forClassName(String fullyQualifiedClassName) {
+ // Never remove the frames from a test class. These will probably be the frame of a failing
+ // assertion.
+ // TODO(cpovirk): This is really only for tests in Truth itself, so this doesn't matter yet,
+ // but.... If the Truth tests someday start calling into nested classes, we may want to add:
+ // || fullyQualifiedClassName.contains("Test$")
+ if (fullyQualifiedClassName.endsWith("Test")) {
+ return StackFrameType.NEVER_REMOVE;
+ }
+
+ for (StackFrameType stackFrameType : StackFrameType.values()) {
+ if (stackFrameType.belongsToType(fullyQualifiedClassName)) {
+ return stackFrameType;
+ }
+ }
+
+ return StackFrameType.NEVER_REMOVE;
+ }
+
+ private final String name;
+ private final ImmutableList<String> prefixes;
+
+ /**
+ * Each type of stack frame has a name of the summary displayed in the cleaned trace.
+ *
+ * <p>Most also have a set of fully qualified class name prefixes that identify when a frame
+ * belongs to this type.
+ */
+ StackFrameType(String name, String... prefixes) {
+ this.name = name;
+ this.prefixes = ImmutableList.copyOf(prefixes);
+ }
+
+ /** Returns the name of this frame type to display in the cleaned trace */
+ String getName() {
+ return name;
+ }
+
+ /**
+ * Returns true if the given frame belongs to this frame type based on the package and/or class
+ * name of the frame.
+ */
+ boolean belongsToType(String fullyQualifiedClassName) {
+ for (String prefix : prefixes) {
+ // TODO(cpovirk): Should we also check prefix + "$"?
+ if (fullyQualifiedClassName.equals(prefix)
+ || fullyQualifiedClassName.startsWith(prefix + ".")) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Returns true if stack trace cleaning is explicitly disabled in a system property. This switch
+ * is intended to be used when attempting to debug the frameworks which are collapsed or filtered
+ * out of stack traces by the cleaner.
+ */
+ private static boolean isStackTraceCleaningDisabled() {
+ // Reading system properties might be forbidden.
+ try {
+ return Boolean.parseBoolean(
+ System.getProperty("com.google.common.truth.disable_stack_trace_cleaning"));
+ } catch (SecurityException e) {
+ // Hope for the best.
+ return false;
+ // TODO(cpovirk): Log a warning? Or is that likely to trigger other violations?
+ }
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/StandardSubjectBuilder.java b/core/src/main/java/com/google/common/truth/StandardSubjectBuilder.java
new file mode 100644
index 00000000..512de7e8
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/StandardSubjectBuilder.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.GwtIncompatible;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multiset;
+import com.google.common.collect.Table;
+import java.math.BigDecimal;
+import java.util.Map;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * In a fluent assertion chain, an object with which you can do any of the following:
+ *
+ * <ul>
+ * <li>Set an optional message with {@link #withMessage}.
+ * <li>Specify the type of {@code Subject} to create with {@link #about(Subject.Factory)}.
+ * <li>For the types of {@code Subject} built into Truth, directly specify the value under test
+ * with {@link #that(Object)}.
+ * </ul>
+ *
+ * <p>For more information about the methods in this class, see <a
+ * href="https://truth.dev/faq#full-chain">this FAQ entry</a>.
+ *
+ * <h3>For people extending Truth</h3>
+ *
+ * <p>You won't extend this type. When you write a custom subject, see <a
+ * href="https://truth.dev/extension">our doc on extensions</a>.
+ */
+public class StandardSubjectBuilder {
+ /**
+ * Returns a new instance that invokes the given {@code FailureStrategy} when a check fails. Most
+ * users should not need this. If you think you do, see the documentation on {@link
+ * FailureStrategy}.
+ */
+ public static StandardSubjectBuilder forCustomFailureStrategy(FailureStrategy failureStrategy) {
+ return new StandardSubjectBuilder(FailureMetadata.forFailureStrategy(failureStrategy));
+ }
+
+ private final FailureMetadata metadataDoNotReferenceDirectly;
+
+ StandardSubjectBuilder(FailureMetadata metadata) {
+ this.metadataDoNotReferenceDirectly = checkNotNull(metadata);
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ public final <ComparableT extends Comparable<?>> ComparableSubject<ComparableT> that(
+ @Nullable ComparableT actual) {
+ return new ComparableSubject(metadata(), actual) {};
+ }
+
+ public final BigDecimalSubject that(@Nullable BigDecimal actual) {
+ return new BigDecimalSubject(metadata(), actual);
+ }
+
+ public final Subject that(@Nullable Object actual) {
+ return new Subject(metadata(), actual);
+ }
+
+ @GwtIncompatible("ClassSubject.java")
+ public final ClassSubject that(@Nullable Class<?> actual) {
+ return new ClassSubject(metadata(), actual);
+ }
+
+ public final ThrowableSubject that(@Nullable Throwable actual) {
+ return new ThrowableSubject(metadata(), actual, "throwable");
+ }
+
+ public final LongSubject that(@Nullable Long actual) {
+ return new LongSubject(metadata(), actual);
+ }
+
+ public final DoubleSubject that(@Nullable Double actual) {
+ return new DoubleSubject(metadata(), actual);
+ }
+
+ public final FloatSubject that(@Nullable Float actual) {
+ return new FloatSubject(metadata(), actual);
+ }
+
+ public final IntegerSubject that(@Nullable Integer actual) {
+ return new IntegerSubject(metadata(), actual);
+ }
+
+ public final BooleanSubject that(@Nullable Boolean actual) {
+ return new BooleanSubject(metadata(), actual);
+ }
+
+ public final StringSubject that(@Nullable String actual) {
+ return new StringSubject(metadata(), actual);
+ }
+
+ public final IterableSubject that(@Nullable Iterable<?> actual) {
+ return new IterableSubject(metadata(), actual);
+ }
+
+ public final <T> ObjectArraySubject<T> that(@Nullable T /*@Nullable*/[] actual) {
+ return new ObjectArraySubject<>(metadata(), actual, "array");
+ }
+
+ public final PrimitiveBooleanArraySubject that(boolean /*@Nullable*/[] actual) {
+ return new PrimitiveBooleanArraySubject(metadata(), actual, "array");
+ }
+
+ public final PrimitiveShortArraySubject that(short /*@Nullable*/[] actual) {
+ return new PrimitiveShortArraySubject(metadata(), actual, "array");
+ }
+
+ public final PrimitiveIntArraySubject that(int /*@Nullable*/[] actual) {
+ return new PrimitiveIntArraySubject(metadata(), actual, "array");
+ }
+
+ public final PrimitiveLongArraySubject that(long /*@Nullable*/[] actual) {
+ return new PrimitiveLongArraySubject(metadata(), actual, "array");
+ }
+
+ public final PrimitiveCharArraySubject that(char /*@Nullable*/[] actual) {
+ return new PrimitiveCharArraySubject(metadata(), actual, "array");
+ }
+
+ public final PrimitiveByteArraySubject that(byte /*@Nullable*/[] actual) {
+ return new PrimitiveByteArraySubject(metadata(), actual, "array");
+ }
+
+ public final PrimitiveFloatArraySubject that(float /*@Nullable*/[] actual) {
+ return new PrimitiveFloatArraySubject(metadata(), actual, "array");
+ }
+
+ public final PrimitiveDoubleArraySubject that(double /*@Nullable*/[] actual) {
+ return new PrimitiveDoubleArraySubject(metadata(), actual, "array");
+ }
+
+ public final GuavaOptionalSubject that(@Nullable Optional<?> actual) {
+ return new GuavaOptionalSubject(metadata(), actual, "optional");
+ }
+
+ public final MapSubject that(@Nullable Map<?, ?> actual) {
+ return new MapSubject(metadata(), actual);
+ }
+
+ public final MultimapSubject that(@Nullable Multimap<?, ?> actual) {
+ return new MultimapSubject(metadata(), actual, "multimap");
+ }
+
+ public final MultisetSubject that(@Nullable Multiset<?> actual) {
+ return new MultisetSubject(metadata(), actual);
+ }
+
+ public final TableSubject that(@Nullable Table<?, ?, ?> actual) {
+ return new TableSubject(metadata(), actual);
+ }
+
+ /**
+ * Returns a new instance that will output the given message before the main failure message. If
+ * this method is called multiple times, the messages will appear in the order that they were
+ * specified.
+ */
+ public final StandardSubjectBuilder withMessage(@Nullable String messageToPrepend) {
+ return withMessage("%s", messageToPrepend);
+ }
+
+ /**
+ * Returns a new instance that will output the given message before the main failure message. If
+ * this method is called multiple times, the messages will appear in the order that they were
+ * specified.
+ *
+ * <p><b>Note:</b> the arguments will be substituted into the format template using {@link
+ * com.google.common.base.Strings#lenientFormat Strings.lenientFormat}. Note this only supports
+ * the {@code %s} specifier.
+ *
+ * @throws IllegalArgumentException if the number of placeholders in the format string does not
+ * equal the number of given arguments
+ */
+ public final StandardSubjectBuilder withMessage(String format, /*@Nullable*/ Object... args) {
+ return new StandardSubjectBuilder(metadata().withMessage(format, args));
+ }
+
+ /**
+ * Given a factory for some {@code Subject} class, returns a builder whose {@code that(actual)}
+ * method creates instances of that class. Created subjects use the previously set failure
+ * strategy and any previously set failure message.
+ */
+ public final <S extends Subject, A> SimpleSubjectBuilder<S, A> about(
+ Subject.Factory<S, A> factory) {
+ return new SimpleSubjectBuilder<>(metadata(), factory);
+ }
+
+ public final <CustomSubjectBuilderT extends CustomSubjectBuilder> CustomSubjectBuilderT about(
+ CustomSubjectBuilder.Factory<CustomSubjectBuilderT> factory) {
+ return factory.createSubjectBuilder(metadata());
+ }
+
+ /**
+ * Reports a failure.
+ *
+ * <p>To set a message, first call {@link #withMessage} (or, more commonly, use the shortcut
+ * {@link Truth#assertWithMessage}).
+ */
+ public final void fail() {
+ metadata().fail(ImmutableList.<Fact>of());
+ }
+
+ private FailureMetadata metadata() {
+ checkStatePreconditions();
+ return metadataDoNotReferenceDirectly;
+ }
+
+ /**
+ * Extension point invoked before every assertion. This allows {@link Expect} to check that it's
+ * been set up properly as a {@code TestRule}.
+ */
+ void checkStatePreconditions() {}
+}
diff --git a/core/src/main/java/com/google/common/truth/StringSubject.java b/core/src/main/java/com/google/common/truth/StringSubject.java
new file mode 100644
index 00000000..d3a10d2f
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/StringSubject.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.Fact.simpleFact;
+
+import com.google.common.annotations.GwtIncompatible;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Propositions for string subjects.
+ *
+ * @author David Saff
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+public class StringSubject extends ComparableSubject<String> {
+ private final String actual;
+
+ /**
+ * Constructor for use by subclasses. If you want to create an instance of this class itself, call
+ * {@link Subject#check(String, Object...) check(...)}{@code .that(actual)}.
+ */
+ protected StringSubject(FailureMetadata metadata, @Nullable String string) {
+ super(metadata, string);
+ this.actual = string;
+ }
+
+ /** @deprecated Use {@link #isEqualTo} instead. String comparison is consistent with equality. */
+ @Override
+ @Deprecated
+ public final void isEquivalentAccordingToCompareTo(String other) {
+ super.isEquivalentAccordingToCompareTo(other);
+ }
+
+ /** Fails if the string does not have the given length. */
+ public void hasLength(int expectedLength) {
+ checkArgument(expectedLength >= 0, "expectedLength(%s) must be >= 0", expectedLength);
+ check("length()").that(actual.length()).isEqualTo(expectedLength);
+ }
+
+ /** Fails if the string is not equal to the zero-length "empty string." */
+ public void isEmpty() {
+ if (actual == null) {
+ failWithActual(simpleFact("expected empty string"));
+ } else if (!actual.isEmpty()) {
+ failWithActual(simpleFact("expected to be empty"));
+ }
+ }
+
+ /** Fails if the string is equal to the zero-length "empty string." */
+ public void isNotEmpty() {
+ if (actual == null) {
+ failWithActual(simpleFact("expected nonempty string"));
+ } else if (actual.isEmpty()) {
+ failWithoutActual(simpleFact("expected not to be empty"));
+ }
+ }
+
+ /** Fails if the string does not contain the given sequence. */
+ public void contains(CharSequence string) {
+ checkNotNull(string);
+ if (actual == null) {
+ failWithActual("expected a string that contains", string);
+ } else if (!actual.contains(string)) {
+ failWithActual("expected to contain", string);
+ }
+ }
+
+ /** Fails if the string contains the given sequence. */
+ public void doesNotContain(CharSequence string) {
+ checkNotNull(string);
+ if (actual == null) {
+ failWithActual("expected a string that does not contain", string);
+ } else if (actual.contains(string)) {
+ failWithActual("expected not to contain", string);
+ }
+ }
+
+ /** Fails if the string does not start with the given string. */
+ public void startsWith(String string) {
+ checkNotNull(string);
+ if (actual == null) {
+ failWithActual("expected a string that starts with", string);
+ } else if (!actual.startsWith(string)) {
+ failWithActual("expected to start with", string);
+ }
+ }
+
+ /** Fails if the string does not end with the given string. */
+ public void endsWith(String string) {
+ checkNotNull(string);
+ if (actual == null) {
+ failWithActual("expected a string that ends with", string);
+ } else if (!actual.endsWith(string)) {
+ failWithActual("expected to end with", string);
+ }
+ }
+
+ /** Fails if the string does not match the given regex. */
+ public void matches(String regex) {
+ checkNotNull(regex);
+ if (actual == null) {
+ failWithActual("expected a string that matches", regex);
+ } else if (!actual.matches(regex)) {
+ if (regex.equals(actual)) {
+ failWithoutActual(
+ fact("expected to match", regex),
+ fact("but was", actual),
+ simpleFact("Looks like you want to use .isEqualTo() for an exact equality assertion."));
+ } else {
+ failWithActual("expected to match", regex);
+ }
+ }
+ }
+
+ /** Fails if the string does not match the given regex. */
+ @GwtIncompatible("java.util.regex.Pattern")
+ public void matches(Pattern regex) {
+ checkNotNull(regex);
+ if (actual == null) {
+ failWithActual("expected a string that matches", regex);
+ } else if (!regex.matcher(actual).matches()) {
+ if (regex.toString().equals(actual)) {
+ failWithoutActual(
+ fact("expected to match", regex),
+ fact("but was", actual),
+ simpleFact(
+ "If you want an exact equality assertion you can escape your regex with"
+ + " Pattern.quote()."));
+ } else {
+ failWithActual("expected to match", regex);
+ }
+ }
+ }
+
+ /** Fails if the string matches the given regex. */
+ public void doesNotMatch(String regex) {
+ checkNotNull(regex);
+ if (actual == null) {
+ failWithActual("expected a string that does not match", regex);
+ } else if (actual.matches(regex)) {
+ failWithActual("expected not to match", regex);
+ }
+ }
+
+ /** Fails if the string matches the given regex. */
+ @GwtIncompatible("java.util.regex.Pattern")
+ public void doesNotMatch(Pattern regex) {
+ checkNotNull(regex);
+ if (actual == null) {
+ failWithActual("expected a string that does not match", regex);
+ } else if (regex.matcher(actual).matches()) {
+ failWithActual("expected not to match", regex);
+ }
+ }
+
+ /** Fails if the string does not contain a match on the given regex. */
+ @GwtIncompatible("java.util.regex.Pattern")
+ public void containsMatch(Pattern regex) {
+ checkNotNull(regex);
+ if (actual == null) {
+ failWithActual("expected a string that contains a match for", regex);
+ } else if (!regex.matcher(actual).find()) {
+ failWithActual("expected to contain a match for", regex);
+ }
+ }
+
+ /** Fails if the string does not contain a match on the given regex. */
+ public void containsMatch(String regex) {
+ checkNotNull(regex);
+ if (actual == null) {
+ failWithActual("expected a string that contains a match for", regex);
+ } else if (!Platform.containsMatch(actual, regex)) {
+ failWithActual("expected to contain a match for", regex);
+ }
+ }
+
+ /** Fails if the string contains a match on the given regex. */
+ @GwtIncompatible("java.util.regex.Pattern")
+ public void doesNotContainMatch(Pattern regex) {
+ checkNotNull(regex);
+ if (actual == null) {
+ failWithActual("expected a string that does not contain a match for", regex);
+ return;
+ }
+ Matcher matcher = regex.matcher(actual);
+ if (matcher.find()) {
+ failWithoutActual(
+ fact("expected not to contain a match for", regex),
+ fact("but contained", matcher.group()),
+ fact("full string", actualCustomStringRepresentationForPackageMembersToCall()));
+ }
+ }
+
+ /** Fails if the string contains a match on the given regex. */
+ public void doesNotContainMatch(String regex) {
+ checkNotNull(regex);
+ if (actual == null) {
+ failWithActual("expected a string that does not contain a match for", regex);
+ } else if (Platform.containsMatch(actual, regex)) {
+ failWithActual("expected not to contain a match for", regex);
+ }
+ }
+
+ /**
+ * Returns a {@link StringSubject}-like instance that will ignore the case of the characters.
+ *
+ * <p>Character equality ignoring case is defined as follows: Characters must be equal either
+ * after calling {@link Character#toLowerCase} or after calling {@link Character#toUpperCase}.
+ * Note that this is independent of any locale.
+ */
+ public CaseInsensitiveStringComparison ignoringCase() {
+ return new CaseInsensitiveStringComparison();
+ }
+
+ /** Case insensitive propositions for string subjects. */
+ public final class CaseInsensitiveStringComparison {
+ private CaseInsensitiveStringComparison() {}
+
+ /**
+ * Fails if the subject is not equal to the given sequence (while ignoring case). For the
+ * purposes of this comparison, two strings are equal if any of the following is true:
+ *
+ * <ul>
+ * <li>they are equal according to {@link String#equalsIgnoreCase}
+ * <li>they are both null
+ * </ul>
+ *
+ * <p>Example: "abc" is equal to "ABC", but not to "abcd".
+ */
+ public void isEqualTo(String expected) {
+ if (actual == null) {
+ if (expected != null) {
+ failWithoutActual(
+ fact("expected a string that is equal to", expected),
+ butWas(),
+ simpleFact("(case is ignored)"));
+ }
+ } else {
+ if (expected == null) {
+ failWithoutActual(
+ fact("expected", "null (null reference)"), butWas(), simpleFact("(case is ignored)"));
+ } else if (!actual.equalsIgnoreCase(expected)) {
+ failWithoutActual(fact("expected", expected), butWas(), simpleFact("(case is ignored)"));
+ }
+ }
+ }
+
+ /**
+ * Fails if the subject is equal to the given string (while ignoring case). The meaning of
+ * equality is the same as for the {@link #isEqualTo} method.
+ */
+ public void isNotEqualTo(String unexpected) {
+ if (actual == null) {
+ if (unexpected == null) {
+ failWithoutActual(
+ fact("expected a string that is not equal to", "null (null reference)"),
+ simpleFact("(case is ignored)"));
+ }
+ } else {
+ if (unexpected != null && actual.equalsIgnoreCase(unexpected)) {
+ failWithoutActual(
+ fact("expected not to be", unexpected), butWas(), simpleFact("(case is ignored)"));
+ }
+ }
+ }
+
+ /** Fails if the string does not contain the given sequence (while ignoring case). */
+ public void contains(CharSequence expectedSequence) {
+ checkNotNull(expectedSequence);
+ String expected = expectedSequence.toString();
+ if (actual == null) {
+ failWithoutActual(
+ fact("expected a string that contains", expected),
+ butWas(),
+ simpleFact("(case is ignored)"));
+ } else if (!containsIgnoreCase(expected)) {
+ failWithoutActual(
+ fact("expected to contain", expected), butWas(), simpleFact("(case is ignored)"));
+ }
+ }
+
+ /** Fails if the string contains the given sequence (while ignoring case). */
+ public void doesNotContain(CharSequence expectedSequence) {
+ checkNotNull(expectedSequence);
+ String expected = expectedSequence.toString();
+ if (actual == null) {
+ failWithoutActual(
+ fact("expected a string that does not contain", expected),
+ butWas(),
+ simpleFact("(case is ignored)"));
+ } else if (containsIgnoreCase(expected)) {
+ failWithoutActual(
+ fact("expected not to contain", expected), butWas(), simpleFact("(case is ignored)"));
+ }
+ }
+
+ private boolean containsIgnoreCase(String string) {
+ if (string.isEmpty()) {
+ // TODO(b/79459427): Fix for J2CL discrepancy when string is empty
+ return true;
+ }
+ String subject = actual;
+ for (int subjectOffset = 0;
+ subjectOffset <= subject.length() - string.length();
+ subjectOffset++) {
+ if (subject.regionMatches(
+ /* ignoreCase = */ true,
+ /* toffset = */ subjectOffset,
+ /* other = */ string,
+ /* ooffset = */ 0,
+ /* len = */ string.length())) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/Subject.java b/core/src/main/java/com/google/common/truth/Subject.java
new file mode 100644
index 00000000..0d1436a6
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/Subject.java
@@ -0,0 +1,1198 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.CaseFormat.LOWER_CAMEL;
+import static com.google.common.base.CaseFormat.UPPER_CAMEL;
+import static com.google.common.base.CharMatcher.whitespace;
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.base.Strings.lenientFormat;
+import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.Fact.simpleFact;
+import static com.google.common.truth.Platform.doubleToString;
+import static com.google.common.truth.Platform.floatToString;
+import static com.google.common.truth.Subject.EqualityCheck.SAME_INSTANCE;
+import static com.google.common.truth.SubjectUtils.accumulate;
+import static com.google.common.truth.SubjectUtils.append;
+import static com.google.common.truth.SubjectUtils.concat;
+import static com.google.common.truth.SubjectUtils.sandwich;
+import static java.util.Arrays.asList;
+
+import com.google.common.base.Function;
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.primitives.Booleans;
+import com.google.common.primitives.Bytes;
+import com.google.common.primitives.Chars;
+import com.google.common.primitives.Ints;
+import com.google.common.primitives.Longs;
+import com.google.common.primitives.Shorts;
+import com.google.common.truth.FailureMetadata.OldAndNewValuesAreSimilar;
+import com.google.errorprone.annotations.DoNotCall;
+import com.google.errorprone.annotations.ForOverride;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * An object that lets you perform checks on the value under test. For example, {@code Subject}
+ * contains {@link #isEqualTo(Object)} and {@link #isInstanceOf(Class)}, and {@link StringSubject}
+ * contains {@link StringSubject#startsWith startsWith(String)}.
+ *
+ * <p>To create a {@code Subject} instance, most users will call an {@link Truth#assertThat
+ * assertThat} method. For information about other ways to create an instance, see <a
+ * href="https://truth.dev/faq#full-chain">this FAQ entry</a>.
+ *
+ * <h3>For people extending Truth</h3>
+ *
+ * <p>For information about writing a custom {@link Subject}, see <a
+ * href="https://truth.dev/extension">our doc on extensions</a>.
+ *
+ * @author David Saff
+ * @author Christian Gruber
+ */
+public class Subject {
+ /**
+ * In a fluent assertion chain, the argument to the common overload of {@link
+ * StandardSubjectBuilder#about(Subject.Factory) about}, the method that specifies what kind of
+ * {@link Subject} to create.
+ *
+ * <p>For more information about the fluent chain, see <a
+ * href="https://truth.dev/faq#full-chain">this FAQ entry</a>.
+ *
+ * <h3>For people extending Truth</h3>
+ *
+ * <p>When you write a custom subject, see <a href="https://truth.dev/extension">our doc on
+ * extensions</a>. It explains where {@code Subject.Factory} fits into the process.
+ */
+ public interface Factory<SubjectT extends Subject, ActualT> {
+ /** Creates a new {@link Subject}. */
+ SubjectT createSubject(FailureMetadata metadata, ActualT actual);
+ }
+
+ private static final FailureStrategy IGNORE_STRATEGY =
+ new FailureStrategy() {
+ @Override
+ public void fail(AssertionError failure) {}
+ };
+
+ private final FailureMetadata metadata;
+ private final Object actual;
+ private String customName = null;
+ private final @Nullable String typeDescriptionOverride;
+
+ /**
+ * Constructor for use by subclasses. If you want to create an instance of this class itself, call
+ * {@link Subject#check(String, Object...) check(...)}{@code .that(actual)}.
+ */
+ protected Subject(FailureMetadata metadata, @Nullable Object actual) {
+ this(metadata, actual, /*typeDescriptionOverride=*/ null);
+ }
+
+ /**
+ * Special constructor that lets subclasses provide a description of the type they're testing. For
+ * example, {@link ThrowableSubject} passes the description "throwable." Normally, Truth is able
+ * to infer this name from the class name. However, if we lack runtime type information (notably,
+ * under j2cl with class metadata off), we might not have access to the original class name.
+ *
+ * <p>We don't expect to make this a public API: Class names are nearly always available. It's
+ * just that we want to be able to run Truth's own tests run with class metadata off, and it's
+ * easier to tweak the subjects to know their own names rather than generalize the tests to accept
+ * obfuscated names.
+ */
+ Subject(
+ FailureMetadata metadata, @Nullable Object actual, @Nullable String typeDescriptionOverride) {
+ this.metadata = metadata.updateForSubject(this);
+ this.actual = actual;
+ this.typeDescriptionOverride = typeDescriptionOverride;
+ }
+
+ /** Fails if the subject is not null. */
+ public void isNull() {
+ standardIsEqualTo(null);
+ }
+
+ /** Fails if the subject is null. */
+ public void isNotNull() {
+ standardIsNotEqualTo(null);
+ }
+
+ /**
+ * Fails if the subject is not equal to the given object. For the purposes of this comparison, two
+ * objects are equal if any of the following is true:
+ *
+ * <ul>
+ * <li>they are equal according to {@link Objects#equal}
+ * <li>they are arrays and are considered equal by the appropriate {@link Arrays#equals}
+ * overload
+ * <li>they are boxed integer types ({@code Byte}, {@code Short}, {@code Character}, {@code
+ * Integer}, or {@code Long}) and they are numerically equal when converted to {@code Long}.
+ * <li>the actual value is a boxed floating-point type ({@code Double} or {@code Float}), the
+ * expected value is an {@code Integer}, and the two are numerically equal when converted to
+ * {@code Double}. (This allows {@code assertThat(someDouble).isEqualTo(0)} to pass.)
+ * </ul>
+ *
+ * <p><b>Note:</b> This method does not test the {@link Object#equals} implementation itself; it
+ * <i>assumes</i> that method is functioning correctly according to its contract. Testing an
+ * {@code equals} implementation requires a utility such as <a
+ * href="https://mvnrepository.com/artifact/com.google.guava/guava-testlib">guava-testlib</a>'s <a
+ * href="https://static.javadoc.io/com.google.guava/guava-testlib/23.0/com/google/common/testing/EqualsTester.html">EqualsTester</a>.
+ *
+ * <p>In some cases, this method might not even call {@code equals}. It may instead perform other
+ * tests that will return the same result as long as {@code equals} is implemented according to
+ * the contract for its type.
+ */
+ /*
+ * TODO(cpovirk): Possibly ban overriding isEqualTo+isNotEqualTo in favor of a
+ * compareForEquality(Object, Object) method. That way, people would need to override only one
+ * method, they would get a ComparisonFailure and other message niceties, and they'd have less to
+ * test.
+ */
+ public void isEqualTo(@Nullable Object expected) {
+ standardIsEqualTo(expected);
+ }
+
+ private void standardIsEqualTo(@Nullable Object expected) {
+ ComparisonResult difference = compareForEquality(expected);
+ if (!difference.valuesAreEqual()) {
+ failEqualityCheck(EqualityCheck.EQUAL, expected, difference);
+ }
+ }
+
+ /**
+ * Fails if the subject is equal to the given object. The meaning of equality is the same as for
+ * the {@link #isEqualTo} method.
+ */
+ public void isNotEqualTo(@Nullable Object unexpected) {
+ standardIsNotEqualTo(unexpected);
+ }
+
+ private void standardIsNotEqualTo(@Nullable Object unexpected) {
+ ComparisonResult difference = compareForEquality(unexpected);
+ if (difference.valuesAreEqual()) {
+ String unexpectedAsString = formatActualOrExpected(unexpected);
+ if (actualCustomStringRepresentation().equals(unexpectedAsString)) {
+ failWithoutActual(fact("expected not to be", unexpectedAsString));
+ } else {
+ failWithoutActual(
+ fact("expected not to be", unexpectedAsString),
+ fact(
+ "but was; string representation of actual value",
+ actualCustomStringRepresentation()));
+ }
+ }
+ }
+
+ /**
+ * Returns whether {@code actual} equals {@code expected} differ and, in some cases, a description
+ * of how they differ.
+ *
+ * <p>The equality check follows the rules described on {@link #isEqualTo}.
+ */
+ private ComparisonResult compareForEquality(@Nullable Object expected) {
+ if (actual == null && expected == null) {
+ return ComparisonResult.equal();
+ } else if (actual == null || expected == null) {
+ return ComparisonResult.differentNoDescription();
+ } else if (actual instanceof byte[] && expected instanceof byte[]) {
+ /*
+ * For a special error message and to use faster Arrays.equals to avoid at least one timeout.
+ *
+ * TODO(cpovirk): For performance, use Arrays.equals for other array types (here and/or in
+ * checkArrayEqualsRecursive)? Exception: double[] and float[], whose GWT implementations I
+ * think may have both false positives and false negatives (so we can't even use Arrays.equals
+ * as a fast path for them, nor deepEquals for an Object[] that might contain them). We would
+ * still fall back to the slower checkArrayEqualsRecursive to produce a nicer failure message
+ * -- but naturally only for tests that are about to fail, when performance matters less.
+ */
+ return checkByteArrayEquals((byte[]) expected, (byte[]) actual);
+ } else if (actual.getClass().isArray() && expected.getClass().isArray()) {
+ return checkArrayEqualsRecursive(expected, actual, "");
+ } else if (isIntegralBoxedPrimitive(actual) && isIntegralBoxedPrimitive(expected)) {
+ return ComparisonResult.fromEqualsResult(integralValue(actual) == integralValue(expected));
+ } else if (actual instanceof Double && expected instanceof Double) {
+ return ComparisonResult.fromEqualsResult(
+ Double.compare((Double) actual, (Double) expected) == 0);
+ } else if (actual instanceof Float && expected instanceof Float) {
+ return ComparisonResult.fromEqualsResult(
+ Float.compare((Float) actual, (Float) expected) == 0);
+ } else if (actual instanceof Double && expected instanceof Integer) {
+ return ComparisonResult.fromEqualsResult(
+ Double.compare((Double) actual, (Integer) expected) == 0);
+ } else if (actual instanceof Float && expected instanceof Integer) {
+ return ComparisonResult.fromEqualsResult(
+ Double.compare((Float) actual, (Integer) expected) == 0);
+ } else {
+ return ComparisonResult.fromEqualsResult(actual == expected || actual.equals(expected));
+ }
+ }
+
+ private static boolean isIntegralBoxedPrimitive(@Nullable Object o) {
+ return o instanceof Byte
+ || o instanceof Short
+ || o instanceof Character
+ || o instanceof Integer
+ || o instanceof Long;
+ }
+
+ private static long integralValue(Object o) {
+ if (o instanceof Character) {
+ return (long) ((Character) o).charValue();
+ } else if (o instanceof Number) {
+ return ((Number) o).longValue();
+ } else {
+ throw new AssertionError(o + " must be either a Character or a Number.");
+ }
+ }
+
+ /** Fails if the subject is not the same instance as the given object. */
+ public final void isSameInstanceAs(@Nullable Object expected) {
+ if (actual != expected) {
+ failEqualityCheck(
+ SAME_INSTANCE,
+ expected,
+ /*
+ * Pass through *whether* the values are equal so that failEqualityCheck() can print that
+ * information. But remove the description of the difference, which is always about
+ * content, since people calling isSameInstanceAs() are explicitly not interested in
+ * content, only object identity.
+ */
+ compareForEquality(expected).withoutDescription());
+ }
+ }
+
+ /** Fails if the subject is the same instance as the given object. */
+ public final void isNotSameInstanceAs(@Nullable Object unexpected) {
+ if (actual == unexpected) {
+ /*
+ * We use actualCustomStringRepresentation() because it might be overridden to be better than
+ * actual.toString()/unexpected.toString().
+ */
+ failWithoutActual(
+ fact("expected not to be specific instance", actualCustomStringRepresentation()));
+ }
+ }
+
+ /** Fails if the subject is not an instance of the given class. */
+ public void isInstanceOf(Class<?> clazz) {
+ if (clazz == null) {
+ throw new NullPointerException("clazz");
+ }
+ if (actual == null) {
+ failWithActual("expected instance of", clazz.getName());
+ return;
+ }
+ if (!Platform.isInstanceOfType(actual, clazz)) {
+ if (classMetadataUnsupported()) {
+ throw new UnsupportedOperationException(
+ actualCustomStringRepresentation()
+ + ", an instance of "
+ + actual.getClass().getName()
+ + ", may or may not be an instance of "
+ + clazz.getName()
+ + ". Under -XdisableClassMetadata, we do not have enough information to tell.");
+ }
+ failWithoutActual(
+ fact("expected instance of", clazz.getName()),
+ fact("but was instance of", actual.getClass().getName()),
+ fact("with value", actualCustomStringRepresentation()));
+ }
+ }
+
+ /** Fails if the subject is an instance of the given class. */
+ public void isNotInstanceOf(Class<?> clazz) {
+ if (clazz == null) {
+ throw new NullPointerException("clazz");
+ }
+ if (classMetadataUnsupported()) {
+ throw new UnsupportedOperationException(
+ "isNotInstanceOf is not supported under -XdisableClassMetadata");
+ }
+ if (actual == null) {
+ return; // null is not an instance of clazz.
+ }
+ if (Platform.isInstanceOfType(actual, clazz)) {
+ failWithActual("expected not to be an instance of", clazz.getName());
+ /*
+ * TODO(cpovirk): Consider including actual.getClass() if it's not clazz itself but only a
+ * subtype.
+ */
+ }
+ }
+
+ /** Fails unless the subject is equal to any element in the given iterable. */
+ public void isIn(Iterable<?> iterable) {
+ if (!Iterables.contains(iterable, actual)) {
+ failWithActual("expected any of", iterable);
+ }
+ }
+
+ /** Fails unless the subject is equal to any of the given elements. */
+ public void isAnyOf(
+ @Nullable Object first, @Nullable Object second, @Nullable Object /*@Nullable*/... rest) {
+ isIn(accumulate(first, second, rest));
+ }
+
+ /** Fails if the subject is equal to any element in the given iterable. */
+ public void isNotIn(Iterable<?> iterable) {
+ if (Iterables.contains(iterable, actual)) {
+ failWithActual("expected not to be any of", iterable);
+ }
+ }
+
+ /** Fails if the subject is equal to any of the given elements. */
+ public void isNoneOf(
+ @Nullable Object first, @Nullable Object second, @Nullable Object /*@Nullable*/... rest) {
+ isNotIn(accumulate(first, second, rest));
+ }
+
+ /** Returns the actual value under test. */
+ final Object actual() {
+ return actual;
+ }
+
+ /**
+ * Supplies the direct string representation of the actual value to other methods which may prefix
+ * or otherwise position it in an error message. This should only be overridden to provide an
+ * improved string representation of the value under test, as it would appear in any given error
+ * message, and should not be used for additional prefixing.
+ *
+ * <p>Subjects should override this with care.
+ *
+ * <p>By default, this returns {@code String.ValueOf(getActualValue())}.
+ */
+ /*
+ * TODO(cpovirk): Consider whether this API pulls its weight. If users want to format the actual
+ * value, maybe they should do so themselves? Of course, they won't have a chance to use a custom
+ * format for inherited implementations like isEqualTo(). But if they want to format the actual
+ * value specially, then it seems likely that they'll want to format the expected value specially,
+ * too. And that applies just as well to APIs like isIn(). Maybe we'll want an API that supports
+ * formatting those values, too (like formatActualOrExpected below)? See also the related
+ * b/70930431. But note that we are likely to use this from FailureMetadata, at least in the short
+ * term, for better or for worse.
+ */
+ @ForOverride
+ protected String actualCustomStringRepresentation() {
+ return formatActualOrExpected(actual);
+ }
+
+ final String actualCustomStringRepresentationForPackageMembersToCall() {
+ return actualCustomStringRepresentation();
+ }
+
+ private String formatActualOrExpected(@Nullable Object o) {
+ if (o instanceof byte[]) {
+ return base16((byte[]) o);
+ } else if (o != null && o.getClass().isArray()) {
+ String wrapped = Iterables.toString(stringableIterable(new Object[] {o}));
+ return wrapped.substring(1, wrapped.length() - 1);
+ } else if (o instanceof Double) {
+ return doubleToString((Double) o);
+ } else if (o instanceof Float) {
+ return floatToString((Float) o);
+ } else {
+ return String.valueOf(o);
+ }
+ }
+
+ // We could add a dep on com.google.common.io, but that seems overkill for base16 encoding
+ private static String base16(byte[] bytes) {
+ StringBuilder sb = new StringBuilder(2 * bytes.length);
+ for (byte b : bytes) {
+ sb.append(hexDigits[(b >> 4) & 0xf]).append(hexDigits[b & 0xf]);
+ }
+ return sb.toString();
+ }
+
+ private static final char[] hexDigits = "0123456789ABCDEF".toCharArray();
+
+ private static Iterable<?> stringableIterable(Object[] array) {
+ return Iterables.transform(asList(array), STRINGIFY);
+ }
+
+ private static final Function<Object, Object> STRINGIFY =
+ new Function<Object, Object>() {
+ @Override
+ public Object apply(@Nullable Object input) {
+ if (input != null && input.getClass().isArray()) {
+ Iterable<?> iterable;
+ if (input.getClass() == boolean[].class) {
+ iterable = Booleans.asList((boolean[]) input);
+ } else if (input.getClass() == int[].class) {
+ iterable = Ints.asList((int[]) input);
+ } else if (input.getClass() == long[].class) {
+ iterable = Longs.asList((long[]) input);
+ } else if (input.getClass() == short[].class) {
+ iterable = Shorts.asList((short[]) input);
+ } else if (input.getClass() == byte[].class) {
+ iterable = Bytes.asList((byte[]) input);
+ } else if (input.getClass() == double[].class) {
+ iterable = doubleArrayAsString((double[]) input);
+ } else if (input.getClass() == float[].class) {
+ iterable = floatArrayAsString((float[]) input);
+ } else if (input.getClass() == char[].class) {
+ iterable = Chars.asList((char[]) input);
+ } else {
+ iterable = Arrays.asList((Object[]) input);
+ }
+ return Iterables.transform(iterable, STRINGIFY);
+ }
+ return input;
+ }
+ };
+
+ /**
+ * The result of comparing two objects for equality. This includes both the "equal"/"not-equal"
+ * bit and, in the case of "not equal," optional facts describing the difference.
+ */
+ private static final class ComparisonResult {
+ /**
+ * If {@code equal} is true, returns an equal result; if false, a non-equal result with no
+ * description.
+ */
+ static ComparisonResult fromEqualsResult(boolean equal) {
+ return equal ? EQUAL : DIFFERENT_NO_DESCRIPTION;
+ }
+
+ /** Returns a non-equal result with the given description. */
+ static ComparisonResult differentWithDescription(Fact... facts) {
+ return new ComparisonResult(ImmutableList.copyOf(facts));
+ }
+
+ /** Returns an equal result. */
+ static ComparisonResult equal() {
+ return EQUAL;
+ }
+
+ /** Returns a non-equal result with no description. */
+ static ComparisonResult differentNoDescription() {
+ return DIFFERENT_NO_DESCRIPTION;
+ }
+
+ private static final ComparisonResult EQUAL = new ComparisonResult(null);
+ private static final ComparisonResult DIFFERENT_NO_DESCRIPTION =
+ new ComparisonResult(ImmutableList.<Fact>of());
+
+ private final @Nullable ImmutableList<Fact> facts;
+
+ private ComparisonResult(ImmutableList<Fact> facts) {
+ this.facts = facts;
+ }
+
+ boolean valuesAreEqual() {
+ return facts == null;
+ }
+
+ ImmutableList<Fact> factsOrEmpty() {
+ return firstNonNull(facts, ImmutableList.<Fact>of());
+ }
+
+ /** Returns an instance with the same "equal"/"not-equal" bit but with no description. */
+ ComparisonResult withoutDescription() {
+ return fromEqualsResult(valuesAreEqual());
+ }
+ }
+
+ /**
+ * Returns null if the arrays are equal. If not equal, returns a string comparing the two arrays,
+ * displaying them in the style "[1, 2, 3]" to supplement the main failure message, which uses the
+ * style "010203."
+ */
+ private static ComparisonResult checkByteArrayEquals(byte[] expected, byte[] actual) {
+ if (Arrays.equals(expected, actual)) {
+ return ComparisonResult.equal();
+ }
+ return ComparisonResult.differentWithDescription(
+ fact("expected", Arrays.toString(expected)), fact("but was", Arrays.toString(actual)));
+ }
+
+ /**
+ * Returns null if the arrays are equal, recursively. If not equal, returns the string of the
+ * index at which they're different.
+ */
+ /*
+ * TODO(cpovirk): Decide whether it's worthwhile to go to this trouble to display the index at
+ * which the arrays differ. If we were to stop doing that, we could mostly delegate to
+ * Arrays.equals() and our float/double arrayEquals methods. (We'd use deepEquals, but it doesn't
+ * have our special double/float handling for GWT.)
+ */
+ private static ComparisonResult checkArrayEqualsRecursive(
+ Object expectedArray, Object actualArray, String lastIndex) {
+ if (expectedArray == actualArray) {
+ return ComparisonResult.equal();
+ }
+ String expectedType = arrayType(expectedArray);
+ String actualType = arrayType(actualArray);
+ if (!expectedType.equals(actualType)) {
+ Fact indexFact =
+ lastIndex.isEmpty() ? simpleFact("wrong type") : fact("wrong type for index", lastIndex);
+ return ComparisonResult.differentWithDescription(
+ indexFact, fact("expected", expectedType), fact("but was", actualType));
+ }
+ int actualLength = Array.getLength(actualArray);
+ int expectedLength = Array.getLength(expectedArray);
+ if (expectedLength != actualLength) {
+ Fact indexFact =
+ lastIndex.isEmpty()
+ ? simpleFact("wrong length")
+ : fact("wrong length for index", lastIndex);
+ return ComparisonResult.differentWithDescription(
+ indexFact, fact("expected", expectedLength), fact("but was", actualLength));
+ }
+ for (int i = 0; i < actualLength; i++) {
+ String index = lastIndex + "[" + i + "]";
+ Object expected = Array.get(expectedArray, i);
+ Object actual = Array.get(actualArray, i);
+ if (actual != null
+ && actual.getClass().isArray()
+ && expected != null
+ && expected.getClass().isArray()) {
+ ComparisonResult result = checkArrayEqualsRecursive(expected, actual, index);
+ if (!result.valuesAreEqual()) {
+ return result;
+ }
+ } else if (!gwtSafeObjectEquals(actual, expected)) {
+ return ComparisonResult.differentWithDescription(fact("differs at index", index));
+ }
+ }
+ return ComparisonResult.equal();
+ }
+
+ private static String arrayType(Object array) {
+ if (array.getClass() == boolean[].class) {
+ return "boolean[]";
+ } else if (array.getClass() == int[].class) {
+ return "int[]";
+ } else if (array.getClass() == long[].class) {
+ return "long[]";
+ } else if (array.getClass() == short[].class) {
+ return "short[]";
+ } else if (array.getClass() == byte[].class) {
+ return "byte[]";
+ } else if (array.getClass() == double[].class) {
+ return "double[]";
+ } else if (array.getClass() == float[].class) {
+ return "float[]";
+ } else if (array.getClass() == char[].class) {
+ return "char[]";
+ } else {
+ return "Object[]";
+ }
+ }
+
+ private static boolean gwtSafeObjectEquals(Object actual, Object expected) {
+ if (actual instanceof Double && expected instanceof Double) {
+ return Double.doubleToLongBits((Double) actual) == Double.doubleToLongBits((Double) expected);
+ } else if (actual instanceof Float && expected instanceof Float) {
+ return Float.floatToIntBits((Float) actual) == Float.floatToIntBits((Float) expected);
+ } else {
+ return Objects.equal(actual, expected);
+ }
+ }
+
+ private static List<String> doubleArrayAsString(double[] items) {
+ List<String> itemAsStrings = new ArrayList<>(items.length);
+ for (double item : items) {
+ itemAsStrings.add(doubleToString(item));
+ }
+ return itemAsStrings;
+ }
+
+ private static List<String> floatArrayAsString(float[] items) {
+ List<String> itemAsStrings = new ArrayList<>(items.length);
+ for (float item : items) {
+ itemAsStrings.add(floatToString(item));
+ }
+ return itemAsStrings;
+ }
+
+ /**
+ * Returns a builder for creating a derived subject but without providing information about how
+ * the derived subject will relate to the current subject. In most cases, you should provide such
+ * information by using {@linkplain #check(String, Object...) the other overload}.
+ *
+ * @deprecated Use {@linkplain #check(String, Object...) the other overload}, which requires you
+ * to supply more information to include in any failure messages.
+ */
+ @Deprecated
+ final StandardSubjectBuilder check() {
+ return new StandardSubjectBuilder(metadata.updateForCheckCall());
+ }
+
+ /**
+ * Returns a builder for creating a derived subject.
+ *
+ * <p>Derived subjects retain the {@link FailureStrategy} and {@linkplain
+ * StandardSubjectBuilder#withMessage messages} of the current subject, and in some cases, they
+ * automatically supplement their failure message with information about the original subject.
+ *
+ * <p>For example, {@link ThrowableSubject#hasMessageThat}, which returns a {@link StringSubject},
+ * is implemented with {@code check("getMessage()").that(actual.getMessage())}.
+ *
+ * <p>The arguments to {@code check} describe how the new subject was derived from the old,
+ * formatted like a chained method call. This allows Truth to include that information in its
+ * failure messages. For example, {@code assertThat(caught).hasCauseThat().hasMessageThat()} will
+ * produce a failure message that includes the string "throwable.getCause().getMessage()," thanks
+ * to internal {@code check} calls that supplied "getCause()" and "getMessage()" as arguments.
+ *
+ * <p>If the method you're delegating to accepts parameters, you can pass {@code check} a format
+ * string. For example, {@link MultimapSubject#valuesForKey} calls {@code
+ * check("valuesForKey(%s)", key)}.
+ *
+ * <p>If you aren't really delegating to an instance method on the actual value -- maybe you're
+ * calling a static method, or you're calling a chain of several methods -- you can supply
+ * whatever string will be most useful to users. For example, if you're delegating to {@code
+ * getOnlyElement(actual.colors())}, you might call {@code check("onlyColor()")}.
+ *
+ * @param format a template with {@code %s} placeholders
+ * @param args the arguments to be inserted into those placeholders
+ */
+ protected final StandardSubjectBuilder check(String format, Object... args) {
+ return doCheck(OldAndNewValuesAreSimilar.DIFFERENT, format, args);
+ }
+
+ // TODO(b/134064106): Figure out a public API for this.
+
+ final StandardSubjectBuilder checkNoNeedToDisplayBothValues(String format, Object... args) {
+ return doCheck(OldAndNewValuesAreSimilar.SIMILAR, format, args);
+ }
+
+ private StandardSubjectBuilder doCheck(
+ OldAndNewValuesAreSimilar valuesAreSimilar, String format, Object[] args) {
+ final LazyMessage message = new LazyMessage(format, args);
+ Function<String, String> descriptionUpdate =
+ new Function<String, String>() {
+ @Override
+ public String apply(String input) {
+ return input + "." + message;
+ }
+ };
+ return new StandardSubjectBuilder(
+ metadata.updateForCheckCall(valuesAreSimilar, descriptionUpdate));
+ }
+
+ /**
+ * Begins a new call chain that ignores any failures. This is useful for subjects that normally
+ * delegate with to other subjects by using {@link #check} but have already reported a failure. In
+ * such cases it may still be necessary to return a {@code Subject} instance even though any
+ * subsequent assertions are meaningless. For example, if a user chains together more {@link
+ * ThrowableSubject#hasCauseThat} calls than the actual exception has causes, {@code hasCauseThat}
+ * returns {@code ignoreCheck().that(... a dummy exception ...)}.
+ */
+ protected final StandardSubjectBuilder ignoreCheck() {
+ return StandardSubjectBuilder.forCustomFailureStrategy(IGNORE_STRATEGY);
+ }
+
+ /**
+ * Fails, reporting a message with two "{@linkplain Fact facts}":
+ *
+ * <ul>
+ * <li><i>key</i>: <i>value</i>
+ * <li>but was: <i>actual value</i>.
+ * </ul>
+ *
+ * <p>This is the simplest failure API. For more advanced needs, see {@linkplain
+ * #failWithActual(Fact, Fact...) the other overload} and {@link #failWithoutActual(Fact, Fact...)
+ * failWithoutActual}.
+ *
+ * <p>Example usage: The check {@code contains(String)} calls {@code failWithActual("expected to
+ * contain", string)}.
+ */
+ protected final void failWithActual(String key, @Nullable Object value) {
+ failWithActual(fact(key, value));
+ }
+
+ /**
+ * Fails, reporting a message with the given facts, followed by an automatically added fact of the
+ * form:
+ *
+ * <ul>
+ * <li>but was: <i>actual value</i>.
+ * </ul>
+ *
+ * <p>If you have only one fact to report (and it's a {@linkplain Fact#fact key-value fact}),
+ * prefer {@linkplain #failWithActual(String, Object) the simpler overload}.
+ *
+ * <p>Example usage: The check {@code isEmpty()} calls {@code failWithActual(simpleFact("expected
+ * to be empty"))}.
+ */
+ protected final void failWithActual(Fact first, Fact... rest) {
+ doFail(sandwich(first, rest, butWas()));
+ }
+
+ // TODO(cpovirk): Consider making this protected if there's a need for it.
+ final void failWithActual(Iterable<Fact> facts) {
+ doFail(append(ImmutableList.copyOf(facts), butWas()));
+ }
+
+ /**
+ * Reports a failure constructing a message from a simple verb.
+ *
+ * @param check the check being asserted
+ * @deprecated Prefer to construct {@link Fact}-style methods, typically by using {@link
+ * #failWithActual(Fact, Fact...) failWithActual}{@code (}{@link Fact#simpleFact
+ * simpleFact(...)}{@code )}. However, if you want to preserve your exact failure message as a
+ * migration aid, you can inline this method (and then inline the resulting method call, as
+ * well).
+ */
+ @Deprecated
+ final void fail(String check) {
+ fail(check, new Object[0]);
+ }
+
+ /**
+ * Assembles a failure message and passes such to the FailureStrategy
+ *
+ * @param verb the check being asserted
+ * @param other the value against which the subject is compared
+ * @deprecated Prefer to construct {@link Fact}-style methods, typically by using {@link
+ * #failWithActual(String, Object)}. However, if you want to preserve your exact failure
+ * message as a migration aid, you can inline this method (and then inline the resulting
+ * method call, as well).
+ */
+ @Deprecated
+ final void fail(String verb, Object other) {
+ fail(verb, new Object[] {other});
+ }
+
+ /**
+ * Assembles a failure message and passes such to the FailureStrategy
+ *
+ * @param verb the check being asserted
+ * @param messageParts the expectations against which the subject is compared
+ * @deprecated Prefer to construct {@link Fact}-style methods, typically by using {@link
+ * #failWithActual(Fact, Fact...)}. However, if you want to preserve your exact failure
+ * message as a migration aid, you can inline this method.
+ */
+ @Deprecated
+ final void fail(String verb, Object... messageParts) {
+ StringBuilder message = new StringBuilder("Not true that <");
+ message.append(actualCustomStringRepresentation()).append("> ").append(verb);
+ for (Object part : messageParts) {
+ message.append(" <").append(part).append(">");
+ }
+ failWithoutActual(simpleFact(message.toString()));
+ }
+
+ enum EqualityCheck {
+ EQUAL("expected"),
+ SAME_INSTANCE("expected specific instance");
+
+ final String keyForExpected;
+
+ EqualityCheck(String keyForExpected) {
+ this.keyForExpected = keyForExpected;
+ }
+ }
+
+ /**
+ * Special version of {@link #failEqualityCheck} for use from {@link IterableSubject}, documented
+ * further there.
+ */
+ final void failEqualityCheckForEqualsWithoutDescription(Object expected) {
+ failEqualityCheck(EqualityCheck.EQUAL, expected, ComparisonResult.differentNoDescription());
+ }
+
+ private void failEqualityCheck(
+ EqualityCheck equalityCheck, Object expected, ComparisonResult difference) {
+ String actualString = actualCustomStringRepresentation();
+ String expectedString = formatActualOrExpected(expected);
+ String actualClass = actual == null ? "(null reference)" : actual.getClass().getName();
+ String expectedClass = expected == null ? "(null reference)" : expected.getClass().getName();
+
+ /*
+ * It's a little odd for expectedString to be formatActualOrExpected(expected) but actualString
+ * *not* to be formatActualOrExpected(actual), since we're going to compare the two. Instead,
+ * actualString is actualCustomStringRepresentation() -- as it is for other assertions, since
+ * users may have overridden that method. While actualCustomStringRepresentation() defaults to
+ * formatActualOrExpected(actual), it's only a default.
+ *
+ * What we really want here is probably to delete actualCustomStringRepresentation() and migrate
+ * users to formatActualOrExpected(actual).
+ */
+ boolean sameToStrings = actualString.equals(expectedString);
+ boolean sameClassNames = actualClass.equals(expectedClass);
+ // TODO(cpovirk): Handle "same class name, different class loader."
+ // `equal` is always false for isEqualTo, but it varies for isSameInstanceAs:
+ boolean equal = difference.valuesAreEqual();
+
+ if (equalityCheck == EqualityCheck.EQUAL
+ && (tryFailForTrailingWhitespaceOnly(expected) || tryFailForEmptyString(expected))) {
+ // tryFailForTrailingWhitespaceOnly or tryFailForEmptyString reported a failure, so we're done
+ return;
+ }
+
+ if (sameToStrings) {
+ if (sameClassNames) {
+ String doppelgangerDescription =
+ equal
+ ? "(different but equal instance of same class with same string representation)"
+ : "(non-equal instance of same class with same string representation)";
+ failEqualityCheckNoComparisonFailure(
+ difference,
+ fact(equalityCheck.keyForExpected, expectedString),
+ fact("but was", doppelgangerDescription));
+ } else {
+ failEqualityCheckNoComparisonFailure(
+ difference,
+ fact(equalityCheck.keyForExpected, expectedString),
+ fact("an instance of", expectedClass),
+ fact("but was", "(non-equal value with same string representation)"),
+ fact("an instance of", actualClass));
+ }
+ } else {
+ if (equalityCheck == EqualityCheck.EQUAL && actual != null && expected != null) {
+ metadata.failEqualityCheck(
+ nameAsFacts(), difference.factsOrEmpty(), expectedString, actualString);
+ } else {
+ failEqualityCheckNoComparisonFailure(
+ difference,
+ fact(equalityCheck.keyForExpected, expectedString),
+ fact("but was", actualString));
+ }
+ }
+ }
+
+ /**
+ * Checks whether the actual and expected values are strings that match except for trailing
+ * whitespace. If so, reports a failure and returns true.
+ */
+ private boolean tryFailForTrailingWhitespaceOnly(Object expected) {
+ if (!(actual instanceof String) || !(expected instanceof String)) {
+ return false;
+ }
+
+ /*
+ * TODO(cpovirk): Consider applying this for non-String types. The danger there is that we don't
+ * know whether toString() (or actualCustomStringRepresentation/formatActualOrExpected) and
+ * equals() are consistent for those types.
+ */
+ String actualString = (String) actual;
+ String expectedString = (String) expected;
+ String actualNoTrailing = whitespace().trimTrailingFrom(actualString);
+ String expectedNoTrailing = whitespace().trimTrailingFrom(expectedString);
+ String expectedTrailing =
+ escapeWhitespace(expectedString.substring(expectedNoTrailing.length()));
+ String actualTrailing = escapeWhitespace(actualString.substring(actualNoTrailing.length()));
+
+ if (!actualNoTrailing.equals(expectedNoTrailing)) {
+ return false;
+ }
+
+ if (actualString.startsWith(expectedString)) {
+ failWithoutActual(
+ fact("expected", expectedString),
+ fact("but contained extra trailing whitespace", actualTrailing));
+ } else if (expectedString.startsWith(actualString)) {
+ failWithoutActual(
+ fact("expected", expectedString),
+ fact("but was missing trailing whitespace", expectedTrailing));
+ } else {
+ failWithoutActual(
+ fact("expected", expectedString),
+ fact("with trailing whitespace", expectedTrailing),
+ fact("but trailing whitespace was", actualTrailing));
+ }
+
+ return true;
+ }
+
+ private static String escapeWhitespace(String in) {
+ StringBuilder out = new StringBuilder();
+ for (char c : in.toCharArray()) {
+ out.append(escapeWhitespace(c));
+ }
+ return out.toString();
+ }
+
+ private static String escapeWhitespace(char c) {
+ switch (c) {
+ case '\t':
+ return "\\t";
+ case '\n':
+ return "\\n";
+ case '\f':
+ return "\\f";
+ case '\r':
+ return "\\r";
+ case ' ':
+ return "␣";
+ default:
+ return new String(asUnicodeHexEscape(c));
+ }
+ }
+
+ /**
+ * Checks whether the actual and expected values are empty strings. If so, reports a failure and
+ * returns true.
+ */
+ private boolean tryFailForEmptyString(Object expected) {
+ if (!(actual instanceof String) || !(expected instanceof String)) {
+ return false;
+ }
+
+ String actualString = (String) actual;
+ String expectedString = (String) expected;
+ if (actualString.isEmpty()) {
+ failWithoutActual(fact("expected", expectedString), simpleFact("but was an empty string"));
+ return true;
+ } else if (expectedString.isEmpty()) {
+ failWithoutActual(simpleFact("expected an empty string"), fact("but was", actualString));
+ return true;
+ }
+
+ // Neither string was empty
+ return false;
+ }
+
+ // From SourceCodeEscapers:
+
+ private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
+
+ private static char[] asUnicodeHexEscape(char c) {
+ // Equivalent to String.format("\\u%04x", (int) c);
+ char[] r = new char[6];
+ r[0] = '\\';
+ r[1] = 'u';
+ r[5] = HEX_DIGITS[c & 0xF];
+ c = (char) (c >>> 4);
+ r[4] = HEX_DIGITS[c & 0xF];
+ c = (char) (c >>> 4);
+ r[3] = HEX_DIGITS[c & 0xF];
+ c = (char) (c >>> 4);
+ r[2] = HEX_DIGITS[c & 0xF];
+ return r;
+ }
+
+ private void failEqualityCheckNoComparisonFailure(ComparisonResult difference, Fact... facts) {
+ // TODO(cpovirk): Is it possible for difference.factsOrEmpty() to be nonempty? If not, remove.
+ doFail(concat(asList(facts), difference.factsOrEmpty()));
+ }
+
+ /**
+ * Assembles a failure message and passes it to the FailureStrategy
+ *
+ * @param verb the check being asserted
+ * @param expected the expectations against which the subject is compared
+ * @param failVerb the failure of the check being asserted
+ * @param actual the actual value the subject was compared against
+ * @deprecated Prefer to construct {@link Fact}-style methods, typically by using {@link
+ * #failWithActual(Fact, Fact...)}. However, if you want to preserve your exact failure
+ * message as a migration aid, you can inline this method.
+ */
+ @Deprecated
+ final void failWithBadResults(String verb, Object expected, String failVerb, Object actual) {
+ String message =
+ lenientFormat(
+ "Not true that <%s> %s <%s>. It %s <%s>",
+ actualCustomStringRepresentation(),
+ verb,
+ expected,
+ failVerb,
+ (actual == null) ? "null reference" : actual);
+ failWithoutActual(simpleFact(message));
+ }
+
+ /**
+ * Assembles a failure message with an alternative representation of the wrapped subject and
+ * passes it to the FailureStrategy
+ *
+ * @param verb the check being asserted
+ * @param expected the expected value of the check
+ * @param actual the custom representation of the subject to be reported in the failure.
+ * @deprecated Prefer to construct {@link Fact}-style methods, typically by using {@link
+ * #failWithoutActual(Fact, Fact...)}. However, if you want to preserve your exact failure
+ * message as a migration aid, you can inline this method.
+ */
+ @Deprecated
+ final void failWithCustomSubject(String verb, Object expected, Object actual) {
+ String message =
+ lenientFormat(
+ "Not true that <%s> %s <%s>",
+ (actual == null) ? "null reference" : actual, verb, expected);
+ failWithoutActual(simpleFact(message));
+ }
+
+ /**
+ * @deprecated Prefer to construct {@link Fact}-style methods, typically by using {@link
+ * #failWithoutActual(Fact, Fact...) failWithoutActual}{@code (}{@link Fact#simpleFact
+ * simpleFact(...)}{@code )}. However, if you want to preserve your exact failure message as a
+ * migration aid, you can inline this method.
+ */
+ @Deprecated
+ final void failWithoutSubject(String check) {
+ String strSubject = this.customName == null ? "the subject" : "\"" + customName + "\"";
+ failWithoutActual(simpleFact(lenientFormat("Not true that %s %s", strSubject, check)));
+ }
+
+ /**
+ * Fails, reporting a message with the given facts, <i>without automatically adding the actual
+ * value.</i>
+ *
+ * <p>Most failure messages should report the actual value, so most checks should call {@link
+ * #failWithActual(Fact, Fact...) failWithActual} instead. However, {@code failWithoutActual} is
+ * useful in some cases:
+ *
+ * <ul>
+ * <li>when the actual value is obvious from the rest of the message. For example, {@code
+ * isNotEmpty()} calls {@code failWithoutActual(simpleFact("expected not to be empty")}.
+ * <li>when the actual value shouldn't come last or should have a different key than the default
+ * of "but was." For example, {@code isNotWithin(...).of(...)} calls {@code
+ * failWithoutActual} so that it can put the expected and actual values together, followed
+ * by the tolerance.
+ * </ul>
+ *
+ * <p>Example usage: The check {@code isEmpty()} calls {@code failWithActual(simpleFact("expected
+ * to be empty"))}.
+ */
+ protected final void failWithoutActual(Fact first, Fact... rest) {
+ doFail(ImmutableList.copyOf(Lists.asList(first, rest)));
+ }
+
+ // TODO(cpovirk): Consider making this protected if there's a need for it.
+ final void failWithoutActual(Iterable<Fact> facts) {
+ doFail(ImmutableList.copyOf(facts));
+ }
+
+ /**
+ * Assembles a failure message without a given subject and passes it to the FailureStrategy
+ *
+ * @param check the check being asserted
+ * @deprecated Prefer to construct {@link Fact}-style methods, typically by using {@link
+ * #failWithoutActual(Fact, Fact...) failWithoutActual}{@code (}{@link Fact#simpleFact
+ * simpleFact(...)}{@code )}. However, if you want to preserve your exact failure message as a
+ * migration aid, you can inline this method (and then inline the resulting method call, as
+ * well).
+ */
+ @Deprecated
+ final void failWithoutActual(String check) {
+ failWithoutSubject(check);
+ }
+
+ /**
+ * @throws UnsupportedOperationException always
+ * @deprecated {@link Object#equals(Object)} is not supported on Truth subjects. If you are
+ * writing a test assertion (actual vs. expected), use {@link #isEqualTo(Object)} instead.
+ */
+ @DoNotCall(
+ "Subject.equals() is not supported. Did you mean to call"
+ + " assertThat(actual).isEqualTo(expected) instead of"
+ + " assertThat(actual).equals(expected)?")
+ @Deprecated
+ @Override
+ public final boolean equals(@Nullable Object o) {
+ throw new UnsupportedOperationException(
+ "Subject.equals() is not supported. Did you mean to call"
+ + " assertThat(actual).isEqualTo(expected) instead of"
+ + " assertThat(actual).equals(expected)?");
+ }
+
+ /**
+ * @throws UnsupportedOperationException always
+ * @deprecated {@link Object#hashCode()} is not supported on Truth subjects.
+ */
+ @DoNotCall("Subject.hashCode() is not supported.")
+ @Deprecated
+ @Override
+ public final int hashCode() {
+ throw new UnsupportedOperationException("Subject.hashCode() is not supported.");
+ }
+
+ /**
+ * @throws UnsupportedOperationException always
+ * @deprecated {@link Object#toString()} is not supported on Truth subjects.
+ */
+ @Deprecated
+ @Override
+ public
+ String toString() {
+ throw new UnsupportedOperationException(
+ "Subject.toString() is not supported. Did you mean to call assertThat(foo.toString())"
+ + " instead of assertThat(foo).toString()?");
+ }
+
+ /**
+ * Returns a "but was: <actual value>" string. This method should be rarely needed, since Truth
+ * inserts a "but was" fact by default for assertions. However, it's occasionally useful for calls
+ * to {@code failWithoutActual} that want a "but was" fact but don't want it to come last, where
+ * Truth inserts it by default.
+ */
+ /*
+ * TODO(cpovirk): Consider giving this protected access.
+ *
+ * It is likely better than what users would otherwise do -- `fact("but was", actual)`, which
+ * ignores actualCustomStringRepresentation() (which is inaccessible outside the package).
+ *
+ * But I want to think more about this. In particular, if people use this to reimplement
+ * isEqualTo(), I would be sad that they're missing out on its normal special handling. That's
+ * probably not enough reason to avoid adding this, but we can hold it back for now.
+ */
+ final Fact butWas() {
+ return fact("but was", actualCustomStringRepresentation());
+ }
+
+ /*
+ * Computed lazily so that we're not doing expensive string operations during every assertion,
+ * only during every failure.
+ */
+ final String typeDescription() {
+ return typeDescriptionOrGuess(getClass(), typeDescriptionOverride);
+ }
+
+ private static String typeDescriptionOrGuess(
+ Class<? extends Subject> clazz, @Nullable String typeDescriptionOverride) {
+ if (typeDescriptionOverride != null) {
+ return typeDescriptionOverride;
+ }
+ /*
+ * j2cl doesn't store enough metadata to know whether "Foo$BarSubject" is a nested class, so it
+ * can't tell whether the simple name is "Foo$BarSubject" or just "BarSubject": b/71808768. It
+ * returns "Foo$BarSubject" to err on the side of preserving information. We want just
+ * "BarSubject," so we strip any likely enclosing type ourselves.
+ */
+ String subjectClass = clazz.getSimpleName().replaceFirst(".*[$]", "");
+ String actualClass =
+ (subjectClass.endsWith("Subject") && !subjectClass.equals("Subject"))
+ ? subjectClass.substring(0, subjectClass.length() - "Subject".length())
+ : "Object";
+ return UPPER_CAMEL.to(LOWER_CAMEL, actualClass);
+ }
+
+ private static boolean classMetadataUnsupported() {
+ // https://github.com/google/truth/issues/198
+ // TODO(cpovirk): Consider whether to remove instanceof tests under GWT entirely.
+ // TODO(cpovirk): Run more Truth tests under GWT, and add tests for this.
+ return String.class.getSuperclass() == null;
+ }
+
+ private void doFail(ImmutableList<Fact> facts) {
+ metadata.fail(prependNameIfAny(facts));
+ }
+
+ private ImmutableList<Fact> prependNameIfAny(ImmutableList<Fact> facts) {
+ return concat(nameAsFacts(), facts);
+ }
+
+ private ImmutableList<Fact> nameAsFacts() {
+ return customName == null
+ ? ImmutableList.<Fact>of()
+ : ImmutableList.of(fact("name", customName));
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/SubjectUtils.java b/core/src/main/java/com/google/common/truth/SubjectUtils.java
new file mode 100644
index 00000000..07ef011f
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/SubjectUtils.java
@@ -0,0 +1,405 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Strings.lenientFormat;
+import static com.google.common.collect.Iterables.isEmpty;
+import static com.google.common.collect.Iterables.transform;
+import static com.google.common.collect.Multisets.immutableEntry;
+
+import com.google.common.base.Equivalence;
+import com.google.common.base.Equivalence.Wrapper;
+import com.google.common.base.Function;
+import com.google.common.base.Objects;
+import com.google.common.base.Optional;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.LinkedHashMultiset;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multiset;
+import com.google.common.collect.SetMultimap;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Utility methods used in {@code Subject} implementors.
+ *
+ * @author Christian Gruber
+ * @author Jens Nyman
+ */
+final class SubjectUtils {
+ private SubjectUtils() {}
+
+ static final String HUMAN_UNDERSTANDABLE_EMPTY_STRING = "\"\" (empty String)";
+
+ static <T> List<T> accumulate(T first, T second, T... rest) {
+ // rest should never be deliberately null, so assume that the caller passed null
+ // in the third position but intended it to be the third element in the array of values.
+ // Javac makes the opposite inference, so handle that here.
+ List<T> items = new ArrayList<T>(2 + ((rest == null) ? 1 : rest.length));
+ items.add(first);
+ items.add(second);
+ if (rest == null) {
+ items.add(null);
+ } else {
+ items.addAll(Arrays.asList(rest));
+ }
+ return items;
+ }
+
+ static <T> int countOf(T t, Iterable<T> items) {
+ int count = 0;
+ for (T item : items) {
+ if (t == null ? (item == null) : t.equals(item)) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ static String countDuplicates(Iterable<?> items) {
+ /*
+ * TODO(cpovirk): Remove brackets after migrating all callers to the new message format. But
+ * will that look OK when we put the result next to a homogeneous type name? If not, maybe move
+ * the homogeneous type name to a separate Fact?
+ */
+ return countDuplicatesToMultiset(items).toStringWithBrackets();
+ }
+
+ static String entryString(Multiset.Entry<?> entry) {
+ int count = entry.getCount();
+ String item = String.valueOf(entry.getElement());
+ return (count > 1) ? item + " [" + count + " copies]" : item;
+ }
+
+ private static <T> NonHashingMultiset<T> countDuplicatesToMultiset(Iterable<T> items) {
+ // We use avoid hashing in case the elements don't have a proper
+ // .hashCode() method (e.g., MessageSet from old versions of protobuf).
+ NonHashingMultiset<T> multiset = new NonHashingMultiset<>();
+ for (T item : items) {
+ multiset.add(item);
+ }
+ return multiset;
+ }
+
+ /**
+ * Makes a String representation of {@code items} with collapsed duplicates and additional class
+ * info.
+ *
+ * <p>Example: {@code countDuplicatesAndAddTypeInfo([1, 2, 2, 3]) == "[1, 2 [3 copies]]
+ * (java.lang.Integer)"} and {@code countDuplicatesAndAddTypeInfo([1, 2L]) == "[1
+ * (java.lang.Integer), 2 (java.lang.Long)]"}.
+ */
+ static String countDuplicatesAndAddTypeInfo(Iterable<?> itemsIterable) {
+ Collection<?> items = iterableToCollection(itemsIterable);
+ Optional<String> homogeneousTypeName = getHomogeneousTypeName(items);
+
+ return homogeneousTypeName.isPresent()
+ ? lenientFormat("%s (%s)", countDuplicates(items), homogeneousTypeName.get())
+ : countDuplicates(addTypeInfoToEveryItem(items));
+ }
+
+ /**
+ * Similar to {@link #countDuplicatesAndAddTypeInfo} and {@link #countDuplicates} but (a) only
+ * adds type info if requested and (b) returns a richer object containing the data.
+ */
+ static DuplicateGroupedAndTyped countDuplicatesAndMaybeAddTypeInfoReturnObject(
+ Iterable<?> itemsIterable, boolean addTypeInfo) {
+ if (addTypeInfo) {
+ Collection<?> items = iterableToCollection(itemsIterable);
+ Optional<String> homogeneousTypeName = getHomogeneousTypeName(items);
+
+ NonHashingMultiset<?> valuesWithCountsAndMaybeTypes =
+ homogeneousTypeName.isPresent()
+ ? countDuplicatesToMultiset(items)
+ : countDuplicatesToMultiset(addTypeInfoToEveryItem(items));
+ return new DuplicateGroupedAndTyped(valuesWithCountsAndMaybeTypes, homogeneousTypeName);
+ } else {
+ return new DuplicateGroupedAndTyped(
+ countDuplicatesToMultiset(itemsIterable),
+ /* homogeneousTypeToDisplay= */ Optional.<String>absent());
+ }
+ }
+
+ private static final class NonHashingMultiset<E> {
+ // This ought to be static, but the generics are easier when I can refer to <E>.
+ private final Function<Multiset.Entry<Wrapper<E>>, Multiset.Entry<?>> unwrapKey =
+ new Function<Multiset.Entry<Wrapper<E>>, Multiset.Entry<?>>() {
+ @Override
+ public Multiset.Entry<?> apply(Multiset.Entry<Wrapper<E>> input) {
+ return immutableEntry(input.getElement().get(), input.getCount());
+ }
+ };
+
+ private final Multiset<Equivalence.Wrapper<E>> contents = LinkedHashMultiset.create();
+
+ void add(E element) {
+ contents.add(EQUALITY_WITHOUT_USING_HASH_CODE.wrap(element));
+ }
+
+ boolean remove(E element) {
+ return contents.remove(EQUALITY_WITHOUT_USING_HASH_CODE.wrap(element));
+ }
+
+ int totalCopies() {
+ return contents.size();
+ }
+
+ boolean isEmpty() {
+ return contents.isEmpty();
+ }
+
+ Iterable<Multiset.Entry<?>> entrySet() {
+ return transform(contents.entrySet(), unwrapKey);
+ }
+
+ String toStringWithBrackets() {
+ List<String> parts = new ArrayList<>();
+ for (Multiset.Entry<?> entry : entrySet()) {
+ parts.add(entryString(entry));
+ }
+ return parts.toString();
+ }
+
+ @Override
+ public String toString() {
+ String withBrackets = toStringWithBrackets();
+ return withBrackets.substring(1, withBrackets.length() - 1);
+ }
+
+ private static final Equivalence<Object> EQUALITY_WITHOUT_USING_HASH_CODE =
+ new Equivalence<Object>() {
+ @Override
+ protected boolean doEquivalent(Object a, Object b) {
+ return Objects.equal(a, b);
+ }
+
+ @Override
+ protected int doHash(Object o) {
+ return 0; // slow but hopefully not much worse than what we get with a flat list
+ }
+ };
+ }
+
+ /**
+ * Missing or unexpected values from a collection assertion, with equal objects grouped together
+ * and, in some cases, type information added. If the type information is present, it is either
+ * present in {@code homogeneousTypeToDisplay} (if all objects have the same type) or appended to
+ * each individual element (if some elements have different types).
+ *
+ * <p>This allows collection assertions to the type information on a separate line from the
+ * elements and even to output different elements on different lines.
+ */
+ static final class DuplicateGroupedAndTyped {
+ final NonHashingMultiset<?> valuesAndMaybeTypes;
+ final Optional<String> homogeneousTypeToDisplay;
+
+ DuplicateGroupedAndTyped(
+ NonHashingMultiset<?> valuesAndMaybeTypes, Optional<String> homogeneousTypeToDisplay) {
+ this.valuesAndMaybeTypes = valuesAndMaybeTypes;
+ this.homogeneousTypeToDisplay = homogeneousTypeToDisplay;
+ }
+
+ int totalCopies() {
+ return valuesAndMaybeTypes.totalCopies();
+ }
+
+ boolean isEmpty() {
+ return valuesAndMaybeTypes.isEmpty();
+ }
+
+ Iterable<Multiset.Entry<?>> entrySet() {
+ return valuesAndMaybeTypes.entrySet();
+ }
+
+ @Override
+ public String toString() {
+ return homogeneousTypeToDisplay.isPresent()
+ ? valuesAndMaybeTypes + " (" + homogeneousTypeToDisplay.get() + ")"
+ : valuesAndMaybeTypes.toString();
+ }
+ }
+
+ /**
+ * Makes a String representation of {@code items} with additional class info.
+ *
+ * <p>Example: {@code iterableToStringWithTypeInfo([1, 2]) == "[1, 2] (java.lang.Integer)"} and
+ * {@code iterableToStringWithTypeInfo([1, 2L]) == "[1 (java.lang.Integer), 2 (java.lang.Long)]"}.
+ */
+ static String iterableToStringWithTypeInfo(Iterable<?> itemsIterable) {
+ Collection<?> items = iterableToCollection(itemsIterable);
+ Optional<String> homogeneousTypeName = getHomogeneousTypeName(items);
+
+ if (homogeneousTypeName.isPresent()) {
+ return lenientFormat("%s (%s)", items, homogeneousTypeName.get());
+ } else {
+ return addTypeInfoToEveryItem(items).toString();
+ }
+ }
+
+ /**
+ * Returns a new collection containing all elements in {@code items} for which there exists at
+ * least one element in {@code itemsToCheck} that has the same {@code toString()} value without
+ * being equal.
+ *
+ * <p>Example: {@code retainMatchingToString([1L, 2L, 2L], [2, 3]) == [2L, 2L]}
+ */
+ static List<Object> retainMatchingToString(Iterable<?> items, Iterable<?> itemsToCheck) {
+ SetMultimap<String, Object> stringValueToItemsToCheck = HashMultimap.create();
+ for (Object itemToCheck : itemsToCheck) {
+ stringValueToItemsToCheck.put(String.valueOf(itemToCheck), itemToCheck);
+ }
+
+ List<Object> result = Lists.newArrayList();
+ for (Object item : items) {
+ for (Object itemToCheck : stringValueToItemsToCheck.get(String.valueOf(item))) {
+ if (!Objects.equal(itemToCheck, item)) {
+ result.add(item);
+ break;
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Returns true if there is a pair of an item from {@code items1} and one in {@code items2} that
+ * has the same {@code toString()} value without being equal.
+ *
+ * <p>Example: {@code hasMatchingToStringPair([1L, 2L], [1]) == true}
+ */
+ static boolean hasMatchingToStringPair(Iterable<?> items1, Iterable<?> items2) {
+ if (isEmpty(items1) || isEmpty(items2)) {
+ return false; // Bail early to avoid calling hashCode() on the elements unnecessarily.
+ }
+ return !retainMatchingToString(items1, items2).isEmpty();
+ }
+
+ static String objectToTypeName(Object item) {
+ // TODO(cpovirk): Merge this with the code in Subject.failEqualityCheck().
+ if (item == null) {
+ // The name "null type" comes from the interface javax.lang.model.type.NullType.
+ return "null type";
+ } else if (item instanceof Map.Entry) {
+ Map.Entry<?, ?> entry = (Map.Entry<?, ?>) item;
+ // Fix for interesting bug when entry.getValue() returns itself b/170390717
+ String valueTypeName =
+ entry.getValue() == entry ? "Map.Entry" : objectToTypeName(entry.getValue());
+
+ return lenientFormat("Map.Entry<%s, %s>", objectToTypeName(entry.getKey()), valueTypeName);
+ } else {
+ return item.getClass().getName();
+ }
+ }
+
+ /**
+ * Returns the name of the single type of all given items or {@link Optional#absent()} if no such
+ * type exists.
+ */
+ private static Optional<String> getHomogeneousTypeName(Iterable<?> items) {
+ Optional<String> homogeneousTypeName = Optional.absent();
+ for (Object item : items) {
+ if (item == null) {
+ /*
+ * TODO(cpovirk): Why? We could have multiple nulls, which would be homogeneous. More
+ * likely, we could have exactly one null, which is still homogeneous. Arguably it's weird
+ * to call a single element "homogeneous" at all, but that's not specific to null.
+ */
+ return Optional.absent();
+ } else if (!homogeneousTypeName.isPresent()) {
+ // This is the first item
+ homogeneousTypeName = Optional.of(objectToTypeName(item));
+ } else if (!objectToTypeName(item).equals(homogeneousTypeName.get())) {
+ // items is a heterogeneous collection
+ return Optional.absent();
+ }
+ }
+ return homogeneousTypeName;
+ }
+
+ private static List<String> addTypeInfoToEveryItem(Iterable<?> items) {
+ List<String> itemsWithTypeInfo = Lists.newArrayList();
+ for (Object item : items) {
+ itemsWithTypeInfo.add(lenientFormat("%s (%s)", item, objectToTypeName(item)));
+ }
+ return itemsWithTypeInfo;
+ }
+
+ static <T> Collection<T> iterableToCollection(Iterable<T> iterable) {
+ if (iterable instanceof Collection) {
+ // Should be safe to assume that any Iterable implementing Collection isn't a one-shot
+ // iterable, right? I sure hope so.
+ return (Collection<T>) iterable;
+ } else {
+ return Lists.newArrayList(iterable);
+ }
+ }
+
+ static <T> List<T> iterableToList(Iterable<T> iterable) {
+ if (iterable instanceof List) {
+ return (List<T>) iterable;
+ } else {
+ return Lists.newArrayList(iterable);
+ }
+ }
+
+ /**
+ * Returns an iterable with all empty strings replaced by a non-empty human understandable
+ * indicator for an empty string.
+ *
+ * <p>Returns the given iterable if it contains no empty strings.
+ */
+ static <T> Iterable<T> annotateEmptyStrings(Iterable<T> items) {
+ if (Iterables.contains(items, "")) {
+ List<T> annotatedItems = Lists.newArrayList();
+ for (T item : items) {
+ if (Objects.equal(item, "")) {
+ // This is a safe cast because know that at least one instance of T (this item) is a
+ // String.
+ @SuppressWarnings("unchecked")
+ T newItem = (T) HUMAN_UNDERSTANDABLE_EMPTY_STRING;
+ annotatedItems.add(newItem);
+ } else {
+ annotatedItems.add(item);
+ }
+ }
+ return annotatedItems;
+ } else {
+ return items;
+ }
+ }
+
+ @SafeVarargs
+ static <E> ImmutableList<E> concat(Iterable<? extends E>... inputs) {
+ return ImmutableList.copyOf(Iterables.concat(inputs));
+ }
+
+ static <E> ImmutableList<E> append(E[] array, E object) {
+ return new ImmutableList.Builder<E>().add(array).add(object).build();
+ }
+
+ static <E> ImmutableList<E> append(ImmutableList<? extends E> list, E object) {
+ return new ImmutableList.Builder<E>().addAll(list).add(object).build();
+ }
+
+ static <E> ImmutableList<E> sandwich(E first, E[] array, E last) {
+ return new ImmutableList.Builder<E>().add(first).add(array).add(last).build();
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/TableSubject.java b/core/src/main/java/com/google/common/truth/TableSubject.java
new file mode 100644
index 00000000..87fcd69f
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/TableSubject.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.truth.Fact.simpleFact;
+
+import com.google.common.collect.Table;
+import com.google.common.collect.Table.Cell;
+import com.google.common.collect.Tables;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Propositions for {@link Table} subjects.
+ *
+ * @author Kurt Alfred Kluever
+ */
+public final class TableSubject extends Subject {
+ private final Table<?, ?, ?> actual;
+
+ TableSubject(FailureMetadata metadata, @Nullable Table<?, ?, ?> table) {
+ super(metadata, table);
+ this.actual = table;
+ }
+
+ /** Fails if the table is not empty. */
+ public void isEmpty() {
+ if (!actual.isEmpty()) {
+ failWithActual(simpleFact("expected to be empty"));
+ }
+ }
+
+ /** Fails if the table is empty. */
+ public void isNotEmpty() {
+ if (actual.isEmpty()) {
+ failWithoutActual(simpleFact("expected not to be empty"));
+ }
+ }
+
+ /** Fails if the table does not have the given size. */
+ public final void hasSize(int expectedSize) {
+ checkArgument(expectedSize >= 0, "expectedSize(%s) must be >= 0", expectedSize);
+ check("size()").that(actual.size()).isEqualTo(expectedSize);
+ }
+
+ /** Fails if the table does not contain a mapping for the given row key and column key. */
+ public void contains(@Nullable Object rowKey, @Nullable Object columnKey) {
+ if (!actual.contains(rowKey, columnKey)) {
+ fail("contains mapping for row/column", rowKey, columnKey);
+ }
+ }
+
+ /** Fails if the table contains a mapping for the given row key and column key. */
+ public void doesNotContain(@Nullable Object rowKey, @Nullable Object columnKey) {
+ if (actual.contains(rowKey, columnKey)) {
+ fail("does not contain mapping for row/column", rowKey, columnKey);
+ }
+ }
+
+ /** Fails if the table does not contain the given cell. */
+ public void containsCell(
+ @Nullable Object rowKey, @Nullable Object colKey, @Nullable Object value) {
+ containsCell(Tables.<Object, Object, Object>immutableCell(rowKey, colKey, value));
+ }
+
+ /** Fails if the table does not contain the given cell. */
+ public void containsCell(Cell<?, ?, ?> cell) {
+ checkNotNull(cell);
+ checkNoNeedToDisplayBothValues("cellSet()").that(actual.cellSet()).contains(cell);
+ }
+
+ /** Fails if the table contains the given cell. */
+ public void doesNotContainCell(
+ @Nullable Object rowKey, @Nullable Object colKey, @Nullable Object value) {
+ doesNotContainCell(Tables.<Object, Object, Object>immutableCell(rowKey, colKey, value));
+ }
+
+ /** Fails if the table contains the given cell. */
+ public void doesNotContainCell(Cell<?, ?, ?> cell) {
+ checkNotNull(cell);
+ checkNoNeedToDisplayBothValues("cellSet()").that(actual.cellSet()).doesNotContain(cell);
+ }
+
+ /** Fails if the table does not contain the given row key. */
+ public void containsRow(@Nullable Object rowKey) {
+ check("rowKeySet()").that(actual.rowKeySet()).contains(rowKey);
+ }
+
+ /** Fails if the table does not contain the given column key. */
+ public void containsColumn(@Nullable Object columnKey) {
+ check("columnKeySet()").that(actual.columnKeySet()).contains(columnKey);
+ }
+
+ /** Fails if the table does not contain the given value. */
+ public void containsValue(@Nullable Object value) {
+ check("values()").that(actual.values()).contains(value);
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/ThrowableSubject.java b/core/src/main/java/com/google/common/truth/ThrowableSubject.java
new file mode 100644
index 00000000..8e0761da
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/ThrowableSubject.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Propositions for {@link Throwable} subjects.
+ *
+ * @author Kurt Alfred Kluever
+ */
+public class ThrowableSubject extends Subject {
+ private final Throwable actual;
+
+ /**
+ * Constructor for use by subclasses. If you want to create an instance of this class itself, call
+ * {@link Subject#check(String, Object...) check(...)}{@code .that(actual)}.
+ */
+ protected ThrowableSubject(FailureMetadata metadata, @Nullable Throwable throwable) {
+ this(metadata, throwable, null);
+ }
+
+ ThrowableSubject(
+ FailureMetadata metadata, @Nullable Throwable throwable, @Nullable String typeDescription) {
+ super(metadata, throwable, typeDescription);
+ this.actual = throwable;
+ }
+
+ /*
+ * TODO(cpovirk): consider a special case for isEqualTo and isSameInstanceAs that adds |expected|
+ * as a suppressed exception
+ */
+
+ /** Returns a {@code StringSubject} to make assertions about the throwable's message. */
+ public final StringSubject hasMessageThat() {
+ StandardSubjectBuilder check = check("getMessage()");
+ if (actual instanceof ErrorWithFacts && ((ErrorWithFacts) actual).facts().size() > 1) {
+ check =
+ check.withMessage(
+ "(Note from Truth: When possible, instead of asserting on the full message, assert"
+ + " about individual facts by using ExpectFailure.assertThat.)");
+ }
+ return check.that(actual.getMessage());
+ }
+
+ /**
+ * Returns a new {@code ThrowableSubject} that supports assertions on this throwable's direct
+ * cause. This method can be invoked repeatedly (e.g. {@code
+ * assertThat(e).hasCauseThat().hasCauseThat()....} to assert on a particular indirect cause.
+ */
+ public final ThrowableSubject hasCauseThat() {
+ // provides a more helpful error message if hasCauseThat() methods are chained too deep
+ // e.g. assertThat(new Exception()).hCT().hCT()....
+ // TODO(diamondm) in keeping with other subjects' behavior this should still NPE if the subject
+ // *itself* is null, since there's no context to lose. See also b/37645583
+ if (actual == null) {
+ check("getCause()")
+ .withMessage("Causal chain is not deep enough - add a .isNotNull() check?")
+ .fail();
+ return ignoreCheck()
+ .that(
+ new Throwable() {
+ @Override
+ public Throwable fillInStackTrace() {
+ setStackTrace(new StackTraceElement[0]); // for old versions of Android
+ return this;
+ }
+ });
+ }
+ return check("getCause()").that(actual.getCause());
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/Truth.gwt.xml b/core/src/main/java/com/google/common/truth/Truth.gwt.xml
new file mode 100644
index 00000000..e0be07a6
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/Truth.gwt.xml
@@ -0,0 +1,31 @@
+<module>
+<source path="">
+ <!-- Hack to keep collect from hiding collect.testing supersource: -->
+ <exclude name="**/testing/**"/>
+</source>
+
+<!--
+ We used to set this only for packages that had manual supersource.
+ That worked everywhere that I know of except for one place:
+ when running the GWT util.concurrent tests under Guava.
+ The problem is that GWT responds poorly to two .gwt.xml files in the same Java package:
+ https://goo.gl/pRV3Yn
+ The summary is that it ignores one file in favor of the other.
+ util.concurrent, like nearly all our packages, has two .gwt.xml files: one for prod and one for tests.
+ util.concurrent, unlike our other packages, has, as of this writing, test supersource but no prod supersource.
+ GWT happens to use the prod .gwt.xml, so it looks for no supersource for tests, either.
+ This causes it to fail to find AtomicLongMapTest.
+ Our workaround is to tell GWT that util.concurrent and all other packages have prod supersource, even if they have none.
+ GWT is happy to ignore us when we specify a nonexistent path.
+ (I hope that this workaround does not cause its own problems in the future.)
+-->
+<super-source path="super"/>
+
+<inherits name="com.google.common.annotations.Annotations" />
+<inherits name="com.google.common.base.Base" />
+<inherits name="com.google.common.collect.Collect" />
+<inherits name="com.google.common.primitives.Primitives" />
+<inherits name="com.google.common.util.concurrent.Concurrent" />
+<inherits name="com.google.gwt.core.Core" />
+<inherits name="com.google.gwt.user.User" />
+</module>
diff --git a/core/src/main/java/com/google/common/truth/Truth.java b/core/src/main/java/com/google/common/truth/Truth.java
new file mode 100644
index 00000000..fa9b5aee
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/Truth.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.GwtIncompatible;
+import com.google.common.base.Optional;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multiset;
+import com.google.common.collect.Table;
+import java.math.BigDecimal;
+import java.util.Map;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * The primary entry point for <a href="https://truth.dev">Truth</a>, a library for fluent test
+ * assertions.
+ *
+ * <p>Compare these example JUnit assertions...
+ *
+ * <pre>{@code
+ * assertEquals(b, a);
+ * assertTrue(c);
+ * assertTrue(d.contains(a));
+ * assertTrue(d.contains(a) && d.contains(b));
+ * assertTrue(d.contains(a) || d.contains(b) || d.contains(c));
+ * }</pre>
+ *
+ * ...to their Truth equivalents...
+ *
+ * <pre>{@code
+ * assertThat(a).isEqualTo(b);
+ * assertThat(c).isTrue();
+ * assertThat(d).contains(a);
+ * assertThat(d).containsAtLeast(a, b);
+ * assertThat(d).containsAnyOf(a, b, c);
+ * }</pre>
+ *
+ * <p>Advantages of Truth:
+ *
+ * <ul>
+ * <li>aligns all the "actual" values on the left
+ * <li>produces more detailed failure messages
+ * <li>provides richer operations (like {@link IterableSubject#containsExactly})
+ * </ul>
+ *
+ * <p>For more information about the methods in this class, see <a
+ * href="https://truth.dev/faq#full-chain">this FAQ entry</a>.
+ *
+ * <h3>For people extending Truth</h3>
+ *
+ * <p>The most common way to extend Truth is to write a custom {@link Subject}. (The other, much
+ * less common way is to write a custom {@link FailureStrategy}.) For more information, visit those
+ * types' docs.
+ *
+ * @author David Saff
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+public final class Truth {
+ private Truth() {}
+
+ private static final FailureStrategy THROW_ASSERTION_ERROR =
+ new FailureStrategy() {
+ @Override
+ public void fail(AssertionError failure) {
+ throw failure;
+ }
+ };
+
+ private static final StandardSubjectBuilder ASSERT =
+ StandardSubjectBuilder.forCustomFailureStrategy(THROW_ASSERTION_ERROR);
+
+ /**
+ * Begins a call chain with the fluent Truth API. If the check made by the chain fails, it will
+ * throw {@link AssertionError}.
+ */
+ public static StandardSubjectBuilder assert_() {
+ return ASSERT;
+ }
+
+ /**
+ * Begins an assertion that, if it fails, will prepend the given message to the failure message.
+ *
+ * <p>This method is a shortcut for {@code assert_().withMessage(...)}.
+ *
+ * <p>To set a message when using a custom subject, use {@code assertWithMessage(...).}{@link
+ * StandardSubjectBuilder#about about(...)}, as discussed in <a
+ * href="https://truth.dev/faq#java8">this FAQ entry</a>.
+ */
+ public static StandardSubjectBuilder assertWithMessage(String messageToPrepend) {
+ return assert_().withMessage(messageToPrepend);
+ }
+
+ /**
+ * Begins an assertion that, if it fails, will prepend the given message to the failure message.
+ *
+ * <p><b>Note:</b> the arguments will be substituted into the format template using {@link
+ * com.google.common.base.Strings#lenientFormat Strings.lenientFormat}. Note this only supports
+ * the {@code %s} specifier.
+ *
+ * <p>This method is a shortcut for {@code assert_().withMessage(...)}.
+ *
+ * <p>To set a message when using a custom subject, use {@code assertWithMessage(...).}{@link
+ * StandardSubjectBuilder#about about(...)}, as discussed in <a
+ * href="https://truth.dev/faq#java8">this FAQ entry</a>.
+ *
+ * @throws IllegalArgumentException if the number of placeholders in the format string does not
+ * equal the number of given arguments
+ */
+ public static StandardSubjectBuilder assertWithMessage(String format, Object... args) {
+ return assert_().withMessage(format, args);
+ }
+
+ /**
+ * Given a factory for some {@code Subject} class, returns a builder whose {@code that(actual)}
+ * method creates instances of that class.
+ */
+ public static <S extends Subject, T> SimpleSubjectBuilder<S, T> assertAbout(
+ Subject.Factory<S, T> factory) {
+ return assert_().about(factory);
+ }
+
+ /**
+ * A generic, advanced method of extension of Truth to new types, which is documented on {@link
+ * CustomSubjectBuilder}. Extension creators should prefer {@link Subject.Factory} if possible.
+ */
+ public static <CustomSubjectBuilderT extends CustomSubjectBuilder>
+ CustomSubjectBuilderT assertAbout(
+ CustomSubjectBuilder.Factory<CustomSubjectBuilderT> factory) {
+ return assert_().about(factory);
+ }
+
+ public static <T extends Comparable<?>> ComparableSubject<T> assertThat(@Nullable T actual) {
+ return assert_().that(actual);
+ }
+
+ public static BigDecimalSubject assertThat(@Nullable BigDecimal actual) {
+ return assert_().that(actual);
+ }
+
+ public static Subject assertThat(@Nullable Object actual) {
+ return assert_().that(actual);
+ }
+
+ @GwtIncompatible("ClassSubject.java")
+ public static ClassSubject assertThat(@Nullable Class<?> actual) {
+ return assert_().that(actual);
+ }
+
+ public static ThrowableSubject assertThat(@Nullable Throwable actual) {
+ return assert_().that(actual);
+ }
+
+ public static LongSubject assertThat(@Nullable Long actual) {
+ return assert_().that(actual);
+ }
+
+ public static DoubleSubject assertThat(@Nullable Double actual) {
+ return assert_().that(actual);
+ }
+
+ public static FloatSubject assertThat(@Nullable Float actual) {
+ return assert_().that(actual);
+ }
+
+ public static IntegerSubject assertThat(@Nullable Integer actual) {
+ return assert_().that(actual);
+ }
+
+ public static BooleanSubject assertThat(@Nullable Boolean actual) {
+ return assert_().that(actual);
+ }
+
+ public static StringSubject assertThat(@Nullable String actual) {
+ return assert_().that(actual);
+ }
+
+ public static IterableSubject assertThat(@Nullable Iterable<?> actual) {
+ return assert_().that(actual);
+ }
+
+ public static <T> ObjectArraySubject<T> assertThat(@Nullable T /*@Nullable*/[] actual) {
+ return assert_().that(actual);
+ }
+
+ public static PrimitiveBooleanArraySubject assertThat(boolean /*@Nullable*/[] actual) {
+ return assert_().that(actual);
+ }
+
+ public static PrimitiveShortArraySubject assertThat(short /*@Nullable*/[] actual) {
+ return assert_().that(actual);
+ }
+
+ public static PrimitiveIntArraySubject assertThat(int /*@Nullable*/[] actual) {
+ return assert_().that(actual);
+ }
+
+ public static PrimitiveLongArraySubject assertThat(long /*@Nullable*/[] actual) {
+ return assert_().that(actual);
+ }
+
+ public static PrimitiveByteArraySubject assertThat(byte /*@Nullable*/[] actual) {
+ return assert_().that(actual);
+ }
+
+ public static PrimitiveCharArraySubject assertThat(char /*@Nullable*/[] actual) {
+ return assert_().that(actual);
+ }
+
+ public static PrimitiveFloatArraySubject assertThat(float /*@Nullable*/[] actual) {
+ return assert_().that(actual);
+ }
+
+ public static PrimitiveDoubleArraySubject assertThat(double /*@Nullable*/[] actual) {
+ return assert_().that(actual);
+ }
+
+ public static GuavaOptionalSubject assertThat(@Nullable Optional<?> actual) {
+ return assert_().that(actual);
+ }
+
+ public static MapSubject assertThat(@Nullable Map<?, ?> actual) {
+ return assert_().that(actual);
+ }
+
+ public static MultimapSubject assertThat(@Nullable Multimap<?, ?> actual) {
+ return assert_().that(actual);
+ }
+
+ public static MultisetSubject assertThat(@Nullable Multiset<?> actual) {
+ return assert_().that(actual);
+ }
+
+ public static TableSubject assertThat(@Nullable Table<?, ?, ?> actual) {
+ return assert_().that(actual);
+ }
+
+ /**
+ * An {@code AssertionError} that (a) always supports a cause, even under old versions of Android
+ * and (b) omits "java.lang.AssertionError:" from the beginning of its toString() representation.
+ */
+ // TODO(cpovirk): Consider eliminating this, adding its functionality to AssertionErrorWithFacts?
+ @SuppressWarnings("OverrideThrowableToString") // We intentionally replace the normal format.
+ static final class SimpleAssertionError extends AssertionError {
+ /** Separate cause field, in case initCause() fails. */
+ private final @Nullable Throwable cause;
+
+ private SimpleAssertionError(String message, @Nullable Throwable cause) {
+ super(checkNotNull(message));
+ this.cause = cause;
+
+ try {
+ initCause(cause);
+ } catch (IllegalStateException alreadyInitializedBecauseOfHarmonyBug) {
+ /*
+ * initCause() throws under old versions of Android:
+ * https://issuetracker.google.com/issues/36945167
+ *
+ * Yes, it's *nice* if initCause() works:
+ *
+ * - It ensures that, if someone tries to call initCause() later, the call will fail loudly
+ * rather than be silently ignored.
+ *
+ * - It populates the usual `Throwable.cause` field, where users of debuggers and other
+ * tools are likely to look first.
+ *
+ * But if it doesn't work, that's fine: Most consumers of the cause should be retrieving it
+ * through getCause(), which we've overridden to return *our* `cause` field, which we've
+ * populated with the correct value.
+ */
+ }
+ }
+
+ static SimpleAssertionError create(String message, @Nullable Throwable cause) {
+ return new SimpleAssertionError(message, cause);
+ }
+
+ static SimpleAssertionError createWithNoStack(String message, @Nullable Throwable cause) {
+ SimpleAssertionError error = create(message, cause);
+ error.setStackTrace(new StackTraceElement[0]);
+ return error;
+ }
+
+ static SimpleAssertionError createWithNoStack(String message) {
+ return createWithNoStack(message, /*cause=*/ null);
+ }
+
+ @Override
+ @SuppressWarnings("UnsynchronizedOverridesSynchronized")
+ public Throwable getCause() {
+ return cause;
+ }
+
+ @Override
+ public String toString() {
+ return getLocalizedMessage();
+ }
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/TruthFailureSubject.java b/core/src/main/java/com/google/common/truth/TruthFailureSubject.java
new file mode 100644
index 00000000..8724f991
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/TruthFailureSubject.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (c) 2018 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.Fact.simpleFact;
+
+import com.google.common.collect.ImmutableList;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Subject for {@link AssertionError} objects thrown by Truth. {@code TruthFailureSubject} contains
+ * methods for asserting about the individual "facts" of those failures. This allows tests to avoid
+ * asserting about the same fact more often than necessary, including avoiding asserting about facts
+ * that are set by other subjects that the main subject delegates to. This keeps tests shorter and
+ * less fragile.
+ *
+ * <p>To create an instance, call {@link ExpectFailure#assertThat}. Or, if you're using a custom
+ * message or failure strategy, pass {@link #truthFailures} to your {@code about(...)} call.
+ *
+ * <p>This class accepts any {@code AssertionError} value, but it will throw an exception if a
+ * caller tries to access the facts of an error that wasn't produced by Truth.
+ */
+public final class TruthFailureSubject extends ThrowableSubject {
+ static final Fact HOW_TO_TEST_KEYS_WITHOUT_VALUES =
+ simpleFact(
+ "To test that a key is present without a value, "
+ + "use factKeys().contains(...) or a similar method.");
+
+ /**
+ * Factory for creating {@link TruthFailureSubject} instances. Most users will just use {@link
+ * ExpectFailure#assertThat}.
+ */
+ public static Factory<TruthFailureSubject, AssertionError> truthFailures() {
+ return FACTORY;
+ }
+
+ private static final Factory<TruthFailureSubject, AssertionError> FACTORY =
+ new Factory<TruthFailureSubject, AssertionError>() {
+ @Override
+ public TruthFailureSubject createSubject(FailureMetadata metadata, AssertionError actual) {
+ return new TruthFailureSubject(metadata, actual, "failure");
+ }
+ };
+
+ private final AssertionError actual;
+
+ TruthFailureSubject(
+ FailureMetadata metadata, @Nullable AssertionError actual, @Nullable String typeDescription) {
+ super(metadata, actual, typeDescription);
+ this.actual = actual;
+ }
+
+ /** Returns a subject for the list of fact keys. */
+ public IterableSubject factKeys() {
+ if (!(actual instanceof ErrorWithFacts)) {
+ failWithActual(simpleFact("expected a failure thrown by Truth's new failure API"));
+ return ignoreCheck().that(ImmutableList.of());
+ }
+ ErrorWithFacts error = (ErrorWithFacts) actual;
+ return check("factKeys()").that(getFactKeys(error));
+ }
+
+ private static ImmutableList<String> getFactKeys(ErrorWithFacts error) {
+ ImmutableList.Builder<String> facts = ImmutableList.builder();
+ for (Fact fact : error.facts()) {
+ facts.add(fact.key);
+ }
+ return facts.build();
+ }
+
+ /**
+ * Returns a subject for the value with the given name.
+ *
+ * <p>The value is always a string, the {@code String.valueOf} representation of the value passed
+ * to {@link Fact#fact}.
+ *
+ * <p>The value is never null:
+ *
+ * <ul>
+ * <li>In the case of {@linkplain Fact#simpleFact facts that have no value}, {@code factValue}
+ * throws an exception. To test for such facts, use {@link #factKeys()}{@code
+ * .contains(...)} or a similar method.
+ * <li>In the case of facts that have a value that is rendered as "null" (such as those created
+ * with {@code fact("key", null)}), {@code factValue} considers them have a string value,
+ * the string "null."
+ * </ul>
+ *
+ * <p>If the failure under test contains more than one fact with the given key, this method will
+ * fail the test. To assert about such a failure, use {@linkplain #factValue(String, int) the
+ * other overload} of {@code factValue}.
+ */
+ public StringSubject factValue(String key) {
+ return doFactValue(key, null);
+ }
+
+ /**
+ * Returns a subject for the value of the {@code index}-th instance of the fact with the given
+ * name. Most Truth failures do not contain multiple facts with the same key, so most tests should
+ * use {@linkplain #factValue(String) the other overload} of {@code factValue}.
+ */
+ public StringSubject factValue(String key, int index) {
+ checkArgument(index >= 0, "index must be nonnegative: %s", index);
+ return doFactValue(key, index);
+ }
+
+ private StringSubject doFactValue(String key, @Nullable Integer index) {
+ checkNotNull(key);
+ if (!(actual instanceof ErrorWithFacts)) {
+ failWithActual(simpleFact("expected a failure thrown by Truth's new failure API"));
+ return ignoreCheck().that("");
+ }
+ ErrorWithFacts error = (ErrorWithFacts) actual;
+
+ /*
+ * We don't care as much about including the actual AssertionError and its facts in these
+ * because the AssertionError will be attached as a cause in nearly all cases.
+ */
+ ImmutableList<Fact> factsWithName = factsWithName(error, key);
+ if (factsWithName.isEmpty()) {
+ failWithoutActual(
+ fact("expected to contain fact", key), fact("but contained only", getFactKeys(error)));
+ return ignoreCheck().that("");
+ }
+ if (index == null && factsWithName.size() > 1) {
+ failWithoutActual(
+ fact("expected to contain a single fact with key", key),
+ fact("but contained multiple", factsWithName));
+ return ignoreCheck().that("");
+ }
+ if (index != null && index > factsWithName.size()) {
+ failWithoutActual(
+ fact("for key", key),
+ fact("index too high", index),
+ fact("fact count was", factsWithName.size()));
+ return ignoreCheck().that("");
+ }
+ String value = factsWithName.get(firstNonNull(index, 0)).value;
+ if (value == null) {
+ if (index == null) {
+ failWithoutActual(
+ simpleFact("expected to have a value"),
+ fact("for key", key),
+ simpleFact("but the key was present with no value"),
+ HOW_TO_TEST_KEYS_WITHOUT_VALUES);
+ } else {
+ failWithoutActual(
+ simpleFact("expected to have a value"),
+ fact("for key", key),
+ fact("and index", index),
+ simpleFact("but the key was present with no value"),
+ HOW_TO_TEST_KEYS_WITHOUT_VALUES);
+ }
+ return ignoreCheck().that("");
+ }
+ StandardSubjectBuilder check =
+ index == null ? check("factValue(%s)", key) : check("factValue(%s, %s)", key, index);
+ return check.that(value);
+ }
+
+ private static ImmutableList<Fact> factsWithName(ErrorWithFacts error, String key) {
+ ImmutableList.Builder<Fact> facts = ImmutableList.builder();
+ for (Fact fact : error.facts()) {
+ if (fact.key.equals(key)) {
+ facts.add(fact);
+ }
+ }
+ return facts.build();
+ }
+}
diff --git a/core/src/main/java/com/google/common/truth/TruthJUnit.java b/core/src/main/java/com/google/common/truth/TruthJUnit.java
new file mode 100644
index 00000000..3c70d887
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/TruthJUnit.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import com.google.common.annotations.GwtIncompatible;
+import org.junit.internal.AssumptionViolatedException;
+
+/**
+ * Provides a way to use Truth to perform JUnit "assumptions." An assumption is a check that, if
+ * false, aborts (skips) the test. This is especially useful in JUnit theories, parameterized tests,
+ * or other combinatorial tests where some subset of the combinations are simply not applicable for
+ * testing.
+ *
+ * <p>For example:
+ *
+ * <pre>{@code
+ * import static com.google.common.truth.Truth.assertThat;
+ * import static com.google.common.truth.TruthJUnit.assume;
+ *
+ * public void @Test testFoosAgainstBars {
+ * assume().that(foo).isNotNull();
+ * assume().that(bar).isNotNull();
+ * assertThat(foo.times(bar)).isEqualTo(blah);
+ * }
+ * }</pre>
+ *
+ * @author David Saff
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+@GwtIncompatible("JUnit4")
+public final class TruthJUnit {
+ private static final FailureStrategy THROW_ASSUMPTION_ERROR =
+ new FailureStrategy() {
+ @Override
+ public void fail(AssertionError failure) {
+ ThrowableAssumptionViolatedException assumptionViolated =
+ new ThrowableAssumptionViolatedException(failure.getMessage(), failure.getCause());
+ assumptionViolated.setStackTrace(failure.getStackTrace());
+ throw assumptionViolated;
+ }
+ };
+
+ private static final StandardSubjectBuilder ASSUME =
+ StandardSubjectBuilder.forCustomFailureStrategy(THROW_ASSUMPTION_ERROR);
+
+ /**
+ * Begins a call chain with the fluent Truth API. If the check made by the chain fails, it will
+ * throw {@link AssumptionViolatedException}.
+ */
+ public static final StandardSubjectBuilder assume() {
+ return ASSUME;
+ }
+
+ // TODO(diamondm): remove this and use org.junit.AssumptionViolatedException once we're on v4.12
+ private static class ThrowableAssumptionViolatedException extends AssumptionViolatedException {
+ public ThrowableAssumptionViolatedException(String message, Throwable throwable) {
+ super(message);
+ if (throwable != null) initCause(throwable);
+ }
+ }
+
+ private TruthJUnit() {}
+}
diff --git a/core/src/main/java/com/google/common/truth/package-info.java b/core/src/main/java/com/google/common/truth/package-info.java
new file mode 100644
index 00000000..89839f73
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/package-info.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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.
+ */
+
+/**
+ * <a href="https://truth.dev" target="_top">Truth</a> is a library for performing assertions in
+ * tests:
+ *
+ * <pre>{@code
+ * assertThat(notificationText).contains("testuser@google.com");
+ * }</pre>
+ *
+ * <p>Truth is owned and maintained by the <a href="http://github.com/google/guava"
+ * target="_top">Guava</a> team. It is used in the majority of the tests in Google’s own codebase.
+ *
+ * <p>For more information, see <a href="https://truth.dev" target="_top">our introduction</a> and
+ * other docs.
+ */
+@CheckReturnValue
+package com.google.common.truth;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/core/src/main/java/com/google/common/truth/super/com/google/common/truth/Platform.java b/core/src/main/java/com/google/common/truth/super/com/google/common/truth/Platform.java
new file mode 100644
index 00000000..ed2eb2c4
--- /dev/null
+++ b/core/src/main/java/com/google/common/truth/super/com/google/common/truth/Platform.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static java.lang.Double.parseDouble;
+import static java.lang.Float.parseFloat;
+import static jsinterop.annotations.JsPackage.GLOBAL;
+
+import com.google.common.collect.ImmutableList;
+import jsinterop.annotations.JsProperty;
+import jsinterop.annotations.JsType;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Extracted routines that need to be swapped in for GWT, to allow for minimal deltas between the
+ * GWT and non-GWT version.
+ *
+ * @author Christian Gruber (cgruber@google.com)
+ */
+final class Platform {
+ private Platform() {}
+
+ /** Returns true if the instance is assignable to the type Clazz. */
+ static boolean isInstanceOfType(Object instance, Class<?> clazz) {
+ if (clazz.isInterface()) {
+ throw new UnsupportedOperationException(
+ "Under GWT, we can't determine whether an object is an instance of an interface Class");
+ }
+
+ for (Class<?> current = instance.getClass();
+ current != null;
+ current = current.getSuperclass()) {
+ if (current.equals(clazz)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ abstract static class PlatformComparisonFailure extends AssertionError {
+ PlatformComparisonFailure(
+ String message,
+ String unusedUnderGwtExpected,
+ String unusedUnderGwtActual,
+ Throwable cause) {
+ super(message, cause);
+ }
+
+ @Override
+ public final String toString() {
+ return getLocalizedMessage();
+ }
+ }
+
+ /** Determines if the given subject contains a match for the given regex. */
+ static boolean containsMatch(String subject, String regex) {
+ return compile(regex).test(subject);
+ }
+
+ /**
+ * Returns an array containing all of the exceptions that were suppressed to deliver the given
+ * exception. Delegates to the getSuppressed() method on Throwable that is available in Java 1.7+
+ */
+ static Throwable[] getSuppressed(Throwable throwable) {
+ return throwable.getSuppressed();
+ }
+
+ static void cleanStackTrace(Throwable throwable) {
+ // Do nothing. See notes in StackTraceCleanerTest.
+ }
+
+ static String inferDescription() {
+ return null;
+ }
+
+ static @Nullable ImmutableList<Fact> makeDiff(String expected, String actual) {
+ /*
+ * IIUC, GWT messages lose their newlines by the time users see them. Given that, users are
+ * likely better served by showing the expected and actual values with mangled newlines than by
+ * showing a diff with mangled newlines (which would look similar but with + and - inserted into
+ * it). Hopefully no one under GWT has long, nearly identical messages. In any case, they've
+ * always been stuck like this.
+ */
+ return null;
+ }
+
+ static String doubleToString(double value) {
+ // This probably doesn't match Java perfectly, but we do our best.
+ if (value == Double.POSITIVE_INFINITY) {
+ return "Infinity";
+ } else if (value == Double.NEGATIVE_INFINITY) {
+ return "-Infinity";
+ } else if (value == 0 && 1 / value < 0) {
+ return "-0.0";
+ } else {
+ // TODO(cpovirk): Would it make more sense to pass `undefined` for the locale? But how?
+ // Then again, we're already hardcoding "Infinity," an English word, above....
+ String result = toLocaleString(value);
+ return (parseDouble(result) == value) ? result : Double.toString(value);
+ }
+ }
+
+ static String floatToString(float value) {
+ // This probably doesn't match Java perfectly, but we do our best.
+ if (value == Float.POSITIVE_INFINITY) {
+ return "Infinity";
+ } else if (value == Float.NEGATIVE_INFINITY) {
+ return "-Infinity";
+ } else if (value == 0 && 1 / value < 0) {
+ return "-0.0";
+ } else if (value == 0) {
+ return "0.0";
+ } else {
+ // TODO(cpovirk): Would it make more sense to pass `undefined` for the locale? But how?
+ // Then again, we're already hardcoding "Infinity," an English word, above....
+ String result = toLocaleString(value);
+ return (parseFloat(result) == value) ? result : Float.toString(value);
+ }
+ }
+
+ private static String toLocaleString(double value) {
+ // Recieve a double as a parameter so that "(Object) value" does not box it.
+ return ((NativeNumber) (Object) value).toLocaleString("en-US", JavaLikeOptions.INSTANCE);
+ }
+
+ /** Tests if current platform is Android which is always false. */
+ static boolean isAndroid() {
+ return false;
+ }
+
+ /** Returns a human readable string representation of the throwable's stack trace. */
+ static String getStackTraceAsString(Throwable throwable) {
+ // TODO(cpovirk): Write a naive implementation that at least dumps the main exception's stack.
+ return throwable.toString();
+ }
+
+ /**
+ * A GWT-swapped version of test rule interface that does nothing. All methods extended from
+ * {@link org.junit.rules.TestRule} needs to be stripped.
+ */
+ interface JUnitTestRule {}
+
+ static final String EXPECT_FAILURE_WARNING_IF_GWT =
+ " Note: One possible reason for a failure not to be caught is for the test to throw some "
+ + "other exception before the failure would have happened. Under GWT, such an exception "
+ + "is hidden by this message. The non-GWT tests do not have this problem, so you may "
+ + "wish to debug them first. If you're still having this problem, consider temporarily "
+ + "modifying the GWT copy of PlatformBaseSubjectTestCase to remove the call to "
+ + "ensureFailureCaught(). Removing that call will let any other exception fall through. "
+ + "(But of course it will also prevent the test from verifying that the expected failure "
+ + "occurred.)";
+
+ // TODO(user): Move this logic to a common location.
+ private static NativeRegExp compile(String pattern) {
+ return new NativeRegExp(pattern);
+ }
+
+ @JsType(isNative = true, name = "RegExp", namespace = GLOBAL)
+ private static class NativeRegExp {
+ public NativeRegExp(String pattern) {}
+
+ public native boolean test(String input);
+ }
+
+ @JsType(isNative = true, name = "Number", namespace = GLOBAL)
+ private interface NativeNumber {
+ String toLocaleString(Object locales, ToLocaleStringOptions options);
+ }
+
+ @JsType(isNative = true, name = "?", namespace = GLOBAL) // "structural type"; see JsType Javadoc
+ private interface ToLocaleStringOptions {
+ @JsProperty
+ int getMinimumFractionDigits();
+
+ @JsProperty
+ int getMaximumFractionDigits();
+
+ @JsProperty
+ boolean getUseGrouping();
+ }
+
+ private static final class JavaLikeOptions implements ToLocaleStringOptions {
+ private static final ToLocaleStringOptions INSTANCE = new JavaLikeOptions();
+
+ @Override
+ public int getMinimumFractionDigits() {
+ return 1;
+ }
+
+ @Override
+ public int getMaximumFractionDigits() {
+ return 20;
+ }
+
+ @Override
+ public boolean getUseGrouping() {
+ return false;
+ }
+ }
+
+ static AssertionError makeComparisonFailure(
+ ImmutableList<String> messages,
+ ImmutableList<Fact> facts,
+ String expected,
+ String actual,
+ @Nullable Throwable cause) {
+ /*
+ * Despite the name, the class we're creating extends AssertionError but not ComparisonFailure
+ * under GWT: See its supertype, PlatformComparisonFailure, above.
+ *
+ * We're actually creating the same class as the non-GWT version of this method does. So why do
+ * we have supersource for this method? It's because we can't run (and, fortunately, don't need
+ * to run) the reflective code we have for non-GWT users, who might or might not choose to
+ * exclude JUnit 4 from their classpath.
+ *
+ * TODO(cpovirk): Remove ComparisonFailureWithFacts and PlatformComparisonFailure entirely under
+ * GWT? That would let us merge them into a single class on the server. And as noted in the
+ * non-GWT copy of Platform, we could consider another custom type that exposes getExpected()
+ * and getActual(), even in the absence of ComparisonFailure. That type would work under GWT.
+ */
+ return new ComparisonFailureWithFacts(messages, facts, expected, actual, cause);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/ActualValueInferenceTest.java b/core/src/test/java/com/google/common/truth/ActualValueInferenceTest.java
new file mode 100644
index 00000000..cc0a441b
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/ActualValueInferenceTest.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (c) 2019 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.ExpectFailure.assertThat;
+import static com.google.common.truth.ExpectFailure.expectFailure;
+
+import com.google.common.annotations.GwtIncompatible;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link ActualValueInference}. */
+@GwtIncompatible // Inference doesn't work under GWT.
+@RunWith(JUnit4.class)
+public final class ActualValueInferenceTest {
+ @Test
+ public void simple() {
+ AssertionError failure;
+
+ failure = expectFailure(whenTesting -> whenTesting.that(staticNoArg()).isEqualTo("b"));
+ assertThat(failure).factValue("value of").isEqualTo("staticNoArg()");
+
+ failure = expectFailure(whenTesting -> whenTesting.that(instanceNoArg()).isEqualTo("b"));
+ assertThat(failure).factValue("value of").isEqualTo("instanceNoArg()");
+
+ failure = expectFailure(whenTesting -> whenTesting.that(staticOneArg(0)).isEqualTo("b"));
+ assertThat(failure).factValue("value of").isEqualTo("staticOneArg(...)");
+
+ failure = expectFailure(whenTesting -> whenTesting.that(instanceOneArg(0)).isEqualTo("b"));
+ assertThat(failure).factValue("value of").isEqualTo("instanceOneArg(...)");
+
+ failure =
+ expectFailure(
+ whenTesting ->
+ whenTesting.that(new ActualValueInferenceTest().instanceOneArg(0)).isEqualTo("b"));
+ assertThat(failure).factValue("value of").isEqualTo("instanceOneArg(...)");
+ }
+
+ @Test
+ public void autoBox() {
+ AssertionError failure;
+
+ failure = expectFailure(whenTesting -> whenTesting.that(someByte()).isEqualTo(1));
+ assertThat(failure).factValue("value of").isEqualTo("someByte()");
+
+ failure = expectFailure(whenTesting -> whenTesting.that(someShort()).isEqualTo(1));
+ assertThat(failure).factValue("value of").isEqualTo("someShort()");
+
+ failure = expectFailure(whenTesting -> whenTesting.that(someInt()).isEqualTo(1));
+ assertThat(failure).factValue("value of").isEqualTo("someInt()");
+
+ failure = expectFailure(whenTesting -> whenTesting.that(someLong()).isEqualTo(1));
+ assertThat(failure).factValue("value of").isEqualTo("someLong()");
+
+ failure = expectFailure(whenTesting -> whenTesting.that(someFloat()).isEqualTo(1));
+ assertThat(failure).factValue("value of").isEqualTo("someFloat()");
+
+ failure = expectFailure(whenTesting -> whenTesting.that(someDouble()).isEqualTo(1));
+ assertThat(failure).factValue("value of").isEqualTo("someDouble()");
+
+ failure = expectFailure(whenTesting -> whenTesting.that(someBoolean()).isEqualTo(true));
+ assertThat(failure).factValue("value of").isEqualTo("someBoolean()");
+
+ failure = expectFailure(whenTesting -> whenTesting.that(someChar()).isEqualTo(1));
+ assertThat(failure).factValue("value of").isEqualTo("someChar()");
+ }
+
+ @Test
+ public void otherValueOfOverloads() {
+ AssertionError failure;
+
+ failure =
+ expectFailure(
+ whenTesting -> whenTesting.that(Integer.valueOf(someNumberString())).isEqualTo(1));
+ assertThat(failure).factKeys().doesNotContain("value of");
+
+ failure =
+ expectFailure(
+ whenTesting -> whenTesting.that(Integer.valueOf(someNumberString(), 16)).isEqualTo(1));
+ assertThat(failure).factKeys().doesNotContain("value of");
+ }
+
+ @Test
+ public void variable() {
+ AssertionError failure;
+
+ failure =
+ expectFailure(
+ whenTesting -> {
+ String s = staticNoArg();
+ whenTesting.that(s).isEqualTo("b");
+ });
+ assertThat(failure).factValue("value of").isEqualTo("staticNoArg()");
+ }
+
+ @Test
+ public void chaining() {
+ AssertionError failure;
+
+ failure =
+ expectFailure(
+ whenTesting -> {
+ whenTesting.that(makeException()).hasMessageThat().isEqualTo("b");
+ });
+ assertThat(failure).factValue("value of").isEqualTo("makeException().getMessage()");
+ }
+
+ @Test
+ public void multipleOnOneLine() {
+ AssertionError failure;
+
+ failure =
+ expectFailure(whenTesting -> whenTesting.that(oneTwoThree()).containsExactly(1).inOrder());
+ assertThat(failure).factValue("value of").isEqualTo("oneTwoThree()");
+
+ failure =
+ expectFailure(
+ whenTesting -> whenTesting.that(oneTwoThree()).containsExactly(1, 3, 2).inOrder());
+ assertThat(failure).factValue("value of").isEqualTo("oneTwoThree()");
+ }
+
+ @Test
+ public void boringNames() {
+ AssertionError failure;
+
+ failure =
+ expectFailure(whenTesting -> whenTesting.that(ImmutableList.of(1, 2)).containsExactly(1));
+ assertThat(failure).factKeys().doesNotContain("value of");
+ }
+
+ @Test
+ public void loop() {
+ AssertionError failure;
+
+ failure =
+ expectFailure(
+ whenTesting -> {
+ for (int i = 0; i < 1; i++) {
+ whenTesting.that(staticNoArg()).isEqualTo("b");
+ }
+ });
+ /*
+ * It would be nice for inference to work on this simple loop, but loops can be much more
+ * complex, so for now, we're conservative.
+ */
+ assertThat(failure).factKeys().doesNotContain("value of");
+ }
+
+ @Test
+ public void tryCatch() {
+ AssertionError failure;
+
+ failure =
+ expectFailure(
+ whenTesting -> {
+ String s;
+ try {
+ s = staticNoArg();
+ } catch (RuntimeException e) {
+ s = instanceNoArg();
+ }
+ whenTesting.that(s).isEqualTo("b");
+ });
+ assertThat(failure).factKeys().doesNotContain("value of");
+ }
+
+ static String staticNoArg() {
+ return "a";
+ }
+
+ String instanceNoArg() {
+ return "a";
+ }
+
+ static String staticOneArg(Object o) {
+ return "a";
+ }
+
+ String instanceOneArg(Object o) {
+ return "a";
+ }
+
+ List<Integer> oneTwoThree() {
+ return ImmutableList.of(1, 2, 3);
+ }
+
+ Exception makeException() {
+ return new Exception("a");
+ }
+
+ byte someByte() {
+ return 0;
+ }
+
+ short someShort() {
+ return 0;
+ }
+
+ int someInt() {
+ return 0;
+ }
+
+ long someLong() {
+ return 0;
+ }
+
+ float someFloat() {
+ return 0;
+ }
+
+ double someDouble() {
+ return 0;
+ }
+
+ boolean someBoolean() {
+ return false;
+ }
+
+ char someChar() {
+ return 0;
+ }
+
+ String someNumberString() {
+ return "0";
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/BaseSubjectTestCase.java b/core/src/test/java/com/google/common/truth/BaseSubjectTestCase.java
new file mode 100644
index 00000000..563e87cb
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/BaseSubjectTestCase.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2017 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.ExpectFailure.assertThat;
+
+/** Base class for truth subject tests to extend. */
+abstract class BaseSubjectTestCase extends PlatformBaseSubjectTestCase {
+ final void assertFailureKeys(String... keys) {
+ assertThatFailure().factKeys().containsExactlyElementsIn(keys).inOrder();
+ }
+
+ final void assertFailureValue(String key, String value) {
+ assertThatFailure().factValue(key).isEqualTo(value);
+ }
+
+ final void assertFailureValueIndexed(String key, int index, String value) {
+ assertThatFailure().factValue(key, index).isEqualTo(value);
+ }
+
+ final TruthFailureSubject assertThatFailure() {
+ return assertThat(expectFailure.getFailure());
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/BigDecimalSubjectTest.java b/core/src/test/java/com/google/common/truth/BigDecimalSubjectTest.java
new file mode 100644
index 00000000..c8f005cd
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/BigDecimalSubjectTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2015 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.math.BigDecimal.TEN;
+
+import java.math.BigDecimal;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for BigDecimal Subjects.
+ *
+ * @author Kurt Alfred Kluever
+ */
+@RunWith(JUnit4.class)
+public class BigDecimalSubjectTest extends BaseSubjectTestCase {
+ @SuppressWarnings("TruthSelfEquals")
+ @Test
+ public void isEqualTo() {
+ // make sure this still works
+ assertThat(TEN).isEqualTo(TEN);
+ }
+
+ @Test
+ public void isEquivalentAccordingToCompareTo() {
+ // make sure this still works
+ assertThat(TEN).isEquivalentAccordingToCompareTo(TEN);
+ }
+
+ @Test
+ public void isEqualToIgnoringScale_bigDecimal() {
+ assertThat(TEN).isEqualToIgnoringScale(TEN);
+ assertThat(TEN).isEqualToIgnoringScale(new BigDecimal(10));
+ expectFailureWhenTestingThat(TEN).isEqualToIgnoringScale(new BigDecimal(3));
+ assertFailureKeys("expected", "but was", "(scale is ignored)");
+ assertFailureValue("expected", "3");
+ assertFailureValue("but was", "10");
+ }
+
+ @Test
+ public void isEqualToIgnoringScale_int() {
+ assertThat(TEN).isEqualToIgnoringScale(10);
+ expectFailureWhenTestingThat(TEN).isEqualToIgnoringScale(3);
+ assertFailureKeys("expected", "but was", "(scale is ignored)");
+ assertFailureValue("expected", "3");
+ assertFailureValue("but was", "10");
+ }
+
+ @Test
+ public void isEqualToIgnoringScale_long() {
+ assertThat(TEN).isEqualToIgnoringScale(10L);
+ expectFailureWhenTestingThat(TEN).isEqualToIgnoringScale(3L);
+ assertFailureKeys("expected", "but was", "(scale is ignored)");
+ assertFailureValue("expected", "3");
+ assertFailureValue("but was", "10");
+ }
+
+ @Test
+ public void isEqualToIgnoringScale_string() {
+ assertThat(TEN).isEqualToIgnoringScale("10");
+ assertThat(TEN).isEqualToIgnoringScale("10.");
+ assertThat(TEN).isEqualToIgnoringScale("10.0");
+ assertThat(TEN).isEqualToIgnoringScale("10.00");
+ expectFailureWhenTestingThat(TEN).isEqualToIgnoringScale("3");
+ assertFailureKeys("expected", "but was", "(scale is ignored)");
+ assertFailureValue("expected", "3");
+ assertFailureValue("but was", "10");
+ }
+
+ private BigDecimalSubject expectFailureWhenTestingThat(BigDecimal actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/BooleanSubjectTest.java b/core/src/test/java/com/google/common/truth/BooleanSubjectTest.java
new file mode 100644
index 00000000..5f13fec4
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/BooleanSubjectTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Boolean Subjects.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+@RunWith(JUnit4.class)
+public class BooleanSubjectTest extends BaseSubjectTestCase {
+
+ @Test
+ public void isTrue() {
+ assertThat(true).isTrue();
+ }
+
+ @Test
+ public void nullIsTrueFailing() {
+ expectFailureWhenTestingThat(null).isTrue();
+ assertFailureKeys("expected", "but was");
+ assertFailureValue("expected", "true");
+ assertFailureValue("but was", "null");
+ }
+
+ @Test
+ public void nullIsFalseFailing() {
+ expectFailureWhenTestingThat(null).isFalse();
+ assertFailureKeys("expected", "but was");
+ assertFailureValue("expected", "false");
+ assertFailureValue("but was", "null");
+ }
+
+ @Test
+ public void isTrueFailing() {
+ expectFailureWhenTestingThat(false).isTrue();
+ assertFailureKeys("expected to be true");
+ }
+
+ @Test
+ public void isFalse() {
+ assertThat(false).isFalse();
+ }
+
+ @Test
+ public void isFalseFailing() {
+ expectFailureWhenTestingThat(true).isFalse();
+ assertFailureKeys("expected to be false");
+ }
+
+ private BooleanSubject expectFailureWhenTestingThat(Boolean actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/ChainingTest.java b/core/src/test/java/com/google/common/truth/ChainingTest.java
new file mode 100644
index 00000000..7d958fa7
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/ChainingTest.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (c) 2018 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Fact.simpleFact;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for chained subjects (produced with {@link Subject#check(String, Object...)}, etc.). */
+@RunWith(JUnit4.class)
+public final class ChainingTest extends BaseSubjectTestCase {
+ private static final Throwable throwable = new Throwable("root");
+
+ @Test
+ public void noChaining() {
+ expectFailureWhenTestingThat("root").isThePresentKingOfFrance();
+ assertNoCause("message");
+ }
+
+ @Test
+ public void oneLevel() {
+ expectFailureWhenTestingThat("root").delegatingTo("child").isThePresentKingOfFrance();
+ assertNoCause("message");
+ }
+
+ @Test
+ public void twoLevels() {
+ expectFailureWhenTestingThat("root")
+ .delegatingTo("child")
+ .delegatingTo("grandchild")
+ .isThePresentKingOfFrance();
+ assertNoCause("message");
+ }
+
+ @Test
+ public void noChainingRootThrowable() {
+ expectFailureWhenTestingThat(throwable).isThePresentKingOfFrance();
+ assertHasCause("message");
+ }
+
+ @Test
+ public void oneLevelRootThrowable() {
+ expectFailureWhenTestingThat(throwable).delegatingTo("child").isThePresentKingOfFrance();
+ assertHasCause("message");
+ }
+
+ @Test
+ public void twoLevelsRootThrowable() {
+ expectFailureWhenTestingThat(throwable)
+ .delegatingTo("child")
+ .delegatingTo("grandchild")
+ .isThePresentKingOfFrance();
+ assertHasCause("message");
+ }
+
+ // e.g., future.failureCause()
+ @Test
+ public void oneLevelDerivedThrowable() {
+ expectFailureWhenTestingThat("root").delegatingTo(throwable).isThePresentKingOfFrance();
+ assertHasCause("message");
+ }
+
+ @Test
+ public void twoLevelsDerivedThrowableMiddle() {
+ expectFailureWhenTestingThat("root")
+ .delegatingTo(throwable)
+ .delegatingTo("grandchild")
+ .isThePresentKingOfFrance();
+ assertHasCause("message");
+ }
+
+ @Test
+ public void twoLevelsDerivedThrowableLast() {
+ expectFailureWhenTestingThat("root")
+ .delegatingTo("child")
+ .delegatingTo(throwable)
+ .isThePresentKingOfFrance();
+ assertHasCause("message");
+ }
+
+ @Test
+ public void oneLevelNamed() {
+ expectFailureWhenTestingThat("root")
+ .delegatingToNamed("child", "child")
+ .isThePresentKingOfFrance();
+ assertNoCause("value of : myObject.child\nmessage\nmyObject was: root");
+ }
+
+ @Test
+ public void twoLevelsNamed() {
+ expectFailureWhenTestingThat("root")
+ .delegatingToNamed("child", "child")
+ .delegatingToNamed("grandchild", "grandchild")
+ .isThePresentKingOfFrance();
+ assertNoCause("value of : myObject.child.grandchild\nmessage\nmyObject was: root");
+ }
+
+ @Test
+ public void twoLevelsOnlyFirstNamed() {
+ expectFailureWhenTestingThat("root")
+ .delegatingToNamed("child", "child")
+ .delegatingTo("grandchild")
+ .isThePresentKingOfFrance();
+ assertNoCause("message\nmyObject was: root");
+ }
+
+ @Test
+ public void twoLevelsOnlySecondNamed() {
+ expectFailureWhenTestingThat("root")
+ .delegatingTo("child")
+ .delegatingToNamed("grandchild", "grandchild")
+ .isThePresentKingOfFrance();
+ assertNoCause("value of : myObject.grandchild\nmessage\nmyObject was: root");
+ }
+
+ @Test
+ public void oneLevelNamedNoNeedToDisplayBoth() {
+ expectFailureWhenTestingThat("root")
+ .delegatingToNamedNoNeedToDisplayBoth("child", "child")
+ .isThePresentKingOfFrance();
+ assertNoCause("value of: myObject.child\nmessage");
+ }
+
+ @Test
+ public void twoLevelsNamedNoNeedToDisplayBoth() {
+ expectFailureWhenTestingThat("root")
+ .delegatingToNamedNoNeedToDisplayBoth("child", "child")
+ .delegatingToNamedNoNeedToDisplayBoth("grandchild", "grandchild")
+ .isThePresentKingOfFrance();
+ assertNoCause("value of: myObject.child.grandchild\nmessage");
+ }
+
+ @Test
+ public void twoLevelsOnlyFirstNamedNoNeedToDisplayBoth() {
+ expectFailureWhenTestingThat("root")
+ .delegatingToNamedNoNeedToDisplayBoth("child", "child")
+ .delegatingTo("grandchild")
+ .isThePresentKingOfFrance();
+ assertNoCause("message");
+ }
+
+ @Test
+ public void twoLevelsOnlySecondNamedNoNeedToDisplayBoth() {
+ expectFailureWhenTestingThat("root")
+ .delegatingTo("child")
+ .delegatingToNamedNoNeedToDisplayBoth("grandchild", "grandchild")
+ .isThePresentKingOfFrance();
+ assertNoCause("value of: myObject.grandchild\nmessage");
+ }
+
+ @Test
+ public void twoLevelsNamedOnlyFirstNoNeedToDisplayBoth() {
+ expectFailureWhenTestingThat("root")
+ .delegatingToNamedNoNeedToDisplayBoth("child", "child")
+ .delegatingToNamed("grandchild", "grandchild")
+ .isThePresentKingOfFrance();
+ assertNoCause("value of : myObject.child.grandchild\nmessage\nmyObject was: root");
+ }
+
+ @Test
+ public void twoLevelsNamedOnlySecondNoNeedToDisplayBoth() {
+ expectFailureWhenTestingThat("root")
+ .delegatingToNamed("child", "child")
+ .delegatingToNamedNoNeedToDisplayBoth("grandchild", "grandchild")
+ .isThePresentKingOfFrance();
+ assertNoCause("value of : myObject.child.grandchild\nmessage\nmyObject was: root");
+ }
+
+ @Test
+ public void namedAndMessage() {
+ expectFailure
+ .whenTesting()
+ .withMessage("prefix")
+ .about(myObjects())
+ .that("root")
+ .delegatingToNamed("child", "child")
+ .isThePresentKingOfFrance();
+ assertNoCause("prefix\nvalue of : myObject.child\nmessage\nmyObject was: root");
+ }
+
+ @Test
+ public void checkFail() {
+ expectFailureWhenTestingThat("root").doCheckFail();
+ assertNoCause("message");
+ }
+
+ @Test
+ public void checkFailWithName() {
+ expectFailureWhenTestingThat("root").doCheckFail("child");
+ assertNoCause("message\nvalue of : myObject.child\nmyObject was: root");
+ }
+
+ @Test
+ public void badFormat() {
+ try {
+ Object unused = assertThat("root").check("%s %s", 1, 2, 3);
+ assert_().fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ /*
+ * TODO(cpovirk): It would be nice to have multiple Subject subclasses so that we know we're
+ * pulling the type from the right link in the chain. But we get some coverage of that from other
+ * tests like MultimapSubjectTest.
+ */
+
+ private static final class MyObjectSubject extends Subject {
+ static final Factory<MyObjectSubject, Object> FACTORY =
+ new Factory<MyObjectSubject, Object>() {
+ @Override
+ public MyObjectSubject createSubject(FailureMetadata metadata, Object actual) {
+ return new MyObjectSubject(metadata, actual);
+ }
+ };
+
+ private MyObjectSubject(FailureMetadata metadata, Object actual) {
+ super(metadata, actual);
+ }
+
+ /** Runs a check that always fails with the generic message "message." */
+ void isThePresentKingOfFrance() {
+ failWithoutActual(simpleFact("message"));
+ }
+
+ void doCheckFail() {
+ check().withMessage("message").fail();
+ }
+
+ void doCheckFail(String name) {
+ check(name).withMessage("message").fail();
+ }
+
+ /**
+ * Returns a new {@code MyObjectSubject} for the given actual value, chaining it to the current
+ * subject with {@link Subject#check}.
+ */
+ MyObjectSubject delegatingTo(Object actual) {
+ return check().about(myObjects()).that(actual);
+ }
+
+ /**
+ * Returns a new {@code MyObjectSubject} for the given actual value, chaining it to the current
+ * subject with {@link Subject#check}.
+ */
+ MyObjectSubject delegatingToNamed(Object actual, String name) {
+ return check(name).about(myObjects()).that(actual);
+ }
+
+ MyObjectSubject delegatingToNamedNoNeedToDisplayBoth(Object actual, String name) {
+ return checkNoNeedToDisplayBothValues(name).about(myObjects()).that(actual);
+ }
+ }
+
+ private static Subject.Factory<MyObjectSubject, Object> myObjects() {
+ return MyObjectSubject.FACTORY;
+ }
+
+ private MyObjectSubject expectFailureWhenTestingThat(Object actual) {
+ return expectFailure.whenTesting().about(myObjects()).that(actual);
+ }
+
+ private void assertNoCause(String message) {
+ assertThatFailure().hasMessageThat().isEqualTo(message);
+ assertThatFailure().hasCauseThat().isNull();
+ }
+
+ private void assertHasCause(String message) {
+ assertThatFailure().hasMessageThat().isEqualTo(message);
+ assertThatFailure().hasCauseThat().isEqualTo(throwable);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/ClassSubjectTest.java b/core/src/test/java/com/google/common/truth/ClassSubjectTest.java
new file mode 100644
index 00000000..777e0cc4
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/ClassSubjectTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for introspective Subject behaviour.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+@RunWith(JUnit4.class)
+public class ClassSubjectTest extends BaseSubjectTestCase {
+ @Test
+ public void testIsAssignableTo_same() {
+ assertThat(String.class).isAssignableTo(String.class);
+ }
+
+ @Test
+ public void testIsAssignableTo_parent() {
+ assertThat(String.class).isAssignableTo(Object.class);
+ assertThat(NullPointerException.class).isAssignableTo(Exception.class);
+ }
+
+ @Test
+ public void testIsAssignableTo_reversed() {
+ expectFailureWhenTestingThat(Object.class).isAssignableTo(String.class);
+ assertFailureValue("expected to be assignable to", "java.lang.String");
+ }
+
+ @Test
+ public void testIsAssignableTo_differentTypes() {
+ expectFailureWhenTestingThat(String.class).isAssignableTo(Exception.class);
+ assertFailureValue("expected to be assignable to", "java.lang.Exception");
+ }
+
+ private ClassSubject expectFailureWhenTestingThat(Class<?> actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/ComparableSubjectCompileTest.java b/core/src/test/java/com/google/common/truth/ComparableSubjectCompileTest.java
new file mode 100644
index 00000000..767fbd0f
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/ComparableSubjectCompileTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertAbout;
+import static com.google.testing.compile.JavaSourceSubjectFactory.javaSource;
+
+import com.google.testing.compile.JavaFileObjects;
+import javax.tools.JavaFileObject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link ComparableSubject} calls that should fail to compile.
+ *
+ * @author Kurt Alfred Kluever
+ */
+@RunWith(JUnit4.class)
+public class ComparableSubjectCompileTest {
+ @Test
+ public void comparableMixedTypesDontCompile() {
+ JavaFileObject file =
+ JavaFileObjects.forSourceLines(
+ "test.MyTest",
+ "package test;",
+ "import static com.google.common.truth.Truth.assertThat;",
+ "class MyTest {",
+ " public void testFoo() {",
+ " assertThat(new ComparableType(3)).isLessThan(\"kak\");",
+ " }",
+ " private static final class ComparableType implements Comparable<ComparableType> {",
+ " private final int wrapped;",
+ " private ComparableType(int toWrap) {",
+ " this.wrapped = toWrap;",
+ " }",
+ " @Override public int compareTo(ComparableType other) {",
+ " return wrapped - ((ComparableType) other).wrapped;",
+ " }",
+ " }",
+ "}");
+
+ assertAbout(javaSource())
+ .that(file)
+ // https://github.com/google/compile-testing/issues/149
+ .withCompilerOptions("-sourcepath", "")
+ .failsToCompile()
+ .withErrorContaining("java.lang.String cannot be converted to test.MyTest.ComparableType")
+ .in(file)
+ .onLine(5);
+ }
+
+ @Test
+ public void rawComparableTypeMixedTypes() {
+ JavaFileObject file =
+ JavaFileObjects.forSourceLines(
+ "test.MyTest",
+ "package test;",
+ "import static com.google.common.truth.Truth.assertThat;",
+ "class MyTest {",
+ " public void testFoo() {",
+ " assertThat(new RawComparableType(3)).isLessThan(\"kak\");",
+ " }",
+ " private static final class RawComparableType implements Comparable {",
+ " private final int wrapped;",
+ " private RawComparableType(int toWrap) {",
+ " this.wrapped = toWrap;",
+ " }",
+ " @Override public int compareTo(Object other) {",
+ " return wrapped - ((RawComparableType) other).wrapped;",
+ " }",
+ " }",
+ "}");
+ assertAbout(javaSource())
+ .that(file)
+ // https://github.com/google/compile-testing/issues/149
+ .withCompilerOptions("-sourcepath", "")
+ .failsToCompile()
+ .withErrorContaining(
+ "java.lang.String cannot be converted to test.MyTest.RawComparableType")
+ .in(file)
+ .onLine(5);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/ComparableSubjectTest.java b/core/src/test/java/com/google/common/truth/ComparableSubjectTest.java
new file mode 100644
index 00000000..86aa41fc
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/ComparableSubjectTest.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.truth.ExpectFailure.assertThat;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.Range;
+import com.google.common.primitives.Ints;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Comparable Subjects.
+ *
+ * @author Kurt Alfred Kluever
+ */
+@RunWith(JUnit4.class)
+public class ComparableSubjectTest extends BaseSubjectTestCase {
+
+ @Test
+ public void testNulls() {
+ try {
+ assertThat(6).isEquivalentAccordingToCompareTo(null);
+ fail();
+ } catch (NullPointerException expected) {
+ }
+ try {
+ assertThat(6).isGreaterThan(null);
+ fail();
+ } catch (NullPointerException expected) {
+ }
+ try {
+ assertThat(6).isLessThan(null);
+ fail();
+ } catch (NullPointerException expected) {
+ }
+ try {
+ assertThat(6).isAtMost(null);
+ fail();
+ } catch (NullPointerException expected) {
+ }
+ try {
+ assertThat(6).isAtLeast(null);
+ fail();
+ } catch (NullPointerException expected) {
+ }
+ }
+
+ @Test
+ public void isInRange() {
+ Range<Integer> oneToFive = Range.closed(1, 5);
+ assertThat(4).isIn(oneToFive);
+
+ expectFailureWhenTestingThat(6).isIn(oneToFive);
+ assertThat(expectFailure.getFailure())
+ .factValue("expected to be in range")
+ .isEqualTo(oneToFive.toString());
+ }
+
+ @Test
+ public void isNotInRange() {
+ Range<Integer> oneToFive = Range.closed(1, 5);
+ assertThat(6).isNotIn(oneToFive);
+
+ expectFailureWhenTestingThat(4).isNotIn(oneToFive);
+ assertThat(expectFailure.getFailure())
+ .factValue("expected not to be in range")
+ .isEqualTo(oneToFive.toString());
+ }
+
+ @Test
+ public void isEquivalentAccordingToCompareTo() {
+ assertThat(new StringComparedByLength("abc"))
+ .isEquivalentAccordingToCompareTo(new StringComparedByLength("xyz"));
+
+ expectFailureWhenTestingThat(new StringComparedByLength("abc"))
+ .isEquivalentAccordingToCompareTo(new StringComparedByLength("abcd"));
+ assertFailureValue("expected value that sorts equal to", "abcd");
+ }
+
+ private static final class StringComparedByLength implements Comparable<StringComparedByLength> {
+ private final String value;
+
+ StringComparedByLength(String value) {
+ this.value = checkNotNull(value);
+ }
+
+ @Override
+ public int compareTo(StringComparedByLength other) {
+ return Ints.compare(value.length(), other.value.length());
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+ }
+
+ @Test
+ public void isGreaterThan_failsEqual() {
+ assertThat(5).isGreaterThan(4);
+
+ expectFailureWhenTestingThat(4).isGreaterThan(4);
+ assertFailureValue("expected to be greater than", "4");
+ }
+
+ @Test
+ public void isGreaterThan_failsSmaller() {
+ expectFailureWhenTestingThat(3).isGreaterThan(4);
+ assertFailureValue("expected to be greater than", "4");
+ }
+
+ @Test
+ public void isLessThan_failsEqual() {
+ assertThat(4).isLessThan(5);
+
+ expectFailureWhenTestingThat(4).isLessThan(4);
+ assertFailureValue("expected to be less than", "4");
+ }
+
+ @Test
+ public void isLessThan_failsGreater() {
+ expectFailureWhenTestingThat(4).isLessThan(3);
+ assertFailureValue("expected to be less than", "3");
+ }
+
+ @Test
+ public void isAtMost() {
+ assertThat(5).isAtMost(5);
+ assertThat(5).isAtMost(6);
+
+ expectFailureWhenTestingThat(4).isAtMost(3);
+ assertFailureValue("expected to be at most", "3");
+ }
+
+ @Test
+ public void isAtLeast() {
+ assertThat(4).isAtLeast(3);
+ assertThat(4).isAtLeast(4);
+
+ expectFailureWhenTestingThat(4).isAtLeast(5);
+ assertFailureValue("expected to be at least", "5");
+ }
+
+ // Brief tests with other comparable types (no negative test cases)
+
+ @Test
+ public void longs() {
+ assertThat(5L).isGreaterThan(4L);
+ assertThat(4L).isLessThan(5L);
+
+ assertThat(4L).isAtMost(4L);
+ assertThat(4L).isAtMost(5L);
+ assertThat(4L).isAtLeast(4L);
+ assertThat(4L).isAtLeast(3L);
+
+ Range<Long> range = Range.closed(2L, 4L);
+ assertThat(3L).isIn(range);
+ assertThat(5L).isNotIn(range);
+ }
+
+ @Test
+ public void strings() {
+ assertThat("kak").isGreaterThan("gak");
+ assertThat("gak").isLessThan("kak");
+
+ assertThat("kak").isAtMost("kak");
+ assertThat("gak").isAtMost("kak");
+ assertThat("kak").isAtLeast("kak");
+ assertThat("kak").isAtLeast("gak");
+
+ Range<String> range = Range.closed("a", "c");
+ assertThat("b").isIn(range);
+ assertThat("d").isNotIn(range);
+ }
+
+ @Test
+ public void comparableType() {
+ assertThat(new ComparableType(4)).isGreaterThan(new ComparableType(3));
+ assertThat(new ComparableType(3)).isLessThan(new ComparableType(4));
+ }
+
+ @Test
+ public void namedComparableType() {
+ assertWithMessage("comparable").that(new ComparableType(2)).isLessThan(new ComparableType(3));
+ }
+
+ private static final class ComparableType implements Comparable<ComparableType> {
+ private final int wrapped;
+
+ private ComparableType(int toWrap) {
+ this.wrapped = toWrap;
+ }
+
+ @Override
+ public int compareTo(ComparableType other) {
+ return wrapped - other.wrapped;
+ }
+ }
+
+ @Test
+ public void rawComparableType() {
+ assertThat(new RawComparableType(3)).isLessThan(new RawComparableType(4));
+ }
+
+ @SuppressWarnings("ComparableType")
+ private static final class RawComparableType implements Comparable {
+ private final int wrapped;
+
+ private RawComparableType(int toWrap) {
+ this.wrapped = toWrap;
+ }
+
+ @Override
+ public int compareTo(Object other) {
+ return wrapped - ((RawComparableType) other).wrapped;
+ }
+
+ @Override
+ public String toString() {
+ return Integer.toString(wrapped);
+ }
+ }
+
+ private <T extends Comparable<? super T>> ComparableSubject<T> expectFailureWhenTestingThat(
+ T actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/ComparisonFailureWithFactsTest.java b/core/src/test/java/com/google/common/truth/ComparisonFailureWithFactsTest.java
new file mode 100644
index 00000000..d53ecff2
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/ComparisonFailureWithFactsTest.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (c) 2018 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Strings.repeat;
+import static com.google.common.testing.SerializableTester.reserialize;
+import static com.google.common.truth.ComparisonFailures.formatExpectedAndActual;
+import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.Fact.simpleFact;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.annotations.GwtIncompatible;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link ComparisonFailureWithFacts}. */
+@RunWith(JUnit4.class)
+public class ComparisonFailureWithFactsTest {
+ @Test
+ public void formatAllDifferent() {
+ runFormatTest(
+ "foo", "bar",
+ "foo", "bar");
+ }
+
+ @Test
+ public void formatShortOverlap() {
+ runFormatTest(
+ "bar", "baz",
+ "bar", "baz");
+ }
+
+ @Test
+ public void formatLongOverlapStart() {
+ runFormatTest(
+ repeat("b", 100) + "aa",
+ repeat("b", 100) + "oo",
+ "
" + repeat("b", 20) + "aa",
+ "
" + repeat("b", 20) + "oo");
+ }
+
+ @Test
+ public void formatLongOverlapEnd() {
+ runFormatTest(
+ "ba" + repeat("r", 100),
+ "fu" + repeat("r", 100),
+ "ba" + repeat("r", 20) + "
",
+ "fu" + repeat("r", 20) + "
");
+ }
+
+ @Test
+ public void formatLongOverlapStartAlsoSmallAtEnd() {
+ runFormatTest(
+ repeat("b", 100) + "aa" + repeat("t", 7),
+ repeat("b", 100) + "oo" + repeat("t", 7),
+ "
" + repeat("b", 20) + "aattttttt",
+ "
" + repeat("b", 20) + "oottttttt");
+ }
+
+ @Test
+ public void formatLongOverlapEndAlsoSmallAtStart() {
+ runFormatTest(
+ repeat("a", 7) + "ba" + repeat("r", 100),
+ repeat("a", 7) + "fu" + repeat("r", 100),
+ "aaaaaaaba" + repeat("r", 20) + "
",
+ "aaaaaaafu" + repeat("r", 20) + "
");
+ }
+
+ @Test
+ public void formatLongOverlapBoth() {
+ runFormatTest(
+ repeat("r", 60) + "a" + repeat("g", 60),
+ repeat("r", 60) + "u" + repeat("g", 60),
+ "
" + repeat("r", 20) + "a" + repeat("g", 20) + "
",
+ "
" + repeat("r", 20) + "u" + repeat("g", 20) + "
");
+ }
+
+ @Test
+ public void formatLongOverlapBothDifferentLength() {
+ runFormatTest(
+ repeat("r", 60) + "aaaaa" + repeat("g", 60),
+ repeat("r", 60) + "u" + repeat("g", 60),
+ "
" + repeat("r", 20) + "aaaaa" + repeat("g", 20) + "
",
+ "
" + repeat("r", 20) + "u" + repeat("g", 20) + "
");
+ }
+
+ @Test
+ public void prefixAndSuffixWouldOverlapSimple() {
+ runFormatTest(
+ repeat("a", 40) + "lmnopqrstuv" + repeat("a", 40),
+ repeat("a", 40) + "lmnopqrstuvlmnopqrstuv" + repeat("a", 40),
+ "
aaaaaaaaalmnopqrstuvaaaaaaaaa
",
+ "
aaaaaaaaalmnopqrstuvlmnopqrstuvaaaaaaaaa
");
+ }
+
+ @Test
+ public void prefixAndSuffixWouldOverlapAllSame() {
+ runFormatTest(repeat("a", 100), repeat("a", 102), "
" + repeat("a", 20), "
" + repeat("a", 22));
+ }
+
+ @Test
+ public void formatNoSplitSurrogateStart() {
+ runFormatTest(
+ repeat("b", 100) + "\uD8AB\uDCAB" + repeat("b", 19) + "aa",
+ repeat("b", 100) + "\uD8AB\uDCAB" + repeat("b", 19) + "oo",
+ "
\uD8AB\uDCAB" + repeat("b", 19) + "aa",
+ "
\uD8AB\uDCAB" + repeat("b", 19) + "oo");
+ }
+
+ @Test
+ public void formatNoSplitSurrogateEnd() {
+ runFormatTest(
+ "ba" + repeat("r", 19) + "\uD8AB\uDCAB" + repeat("r", 100),
+ "fu" + repeat("r", 19) + "\uD8AB\uDCAB" + repeat("r", 100),
+ "ba" + repeat("r", 19) + "\uD8AB\uDCAB
",
+ "fu" + repeat("r", 19) + "\uD8AB\uDCAB
");
+ }
+
+ @GwtIncompatible
+ @Test
+ public void formatDiffOmitStart() {
+ runFormatTest(
+ repeat("a\n", 100) + "b",
+ repeat("a\n", 100) + "c",
+ Joiner.on('\n').join("@@ -98,4 +98,4 @@", " a", " a", " a", "-b", "+c"));
+ }
+
+ @GwtIncompatible
+ @Test
+ public void formatDiffOmitEnd() {
+ runFormatTest(
+ "a" + repeat("\nz", 100),
+ "b" + repeat("\nz", 100),
+ Joiner.on('\n').join("@@ -1,4 +1,4 @@", "-a", "+b", " z", " z", " z"));
+ }
+
+ @GwtIncompatible
+ @Test
+ public void formatDiffOmitBoth() {
+ runFormatTest(
+ repeat("a\n", 100) + "m" + repeat("\nz", 100),
+ repeat("a\n", 100) + "n" + repeat("\nz", 100),
+ Joiner.on('\n').join("@@ -98,7 +98,7 @@", " a", " a", " a", "-m", "+n", " z", " z", " z"));
+ }
+
+ @GwtIncompatible
+ @Test
+ public void formatDiffOmitBothMultipleDifferingLines() {
+ runFormatTest(
+ repeat("a\n", 100) + "m\nn\no\np" + repeat("\nz", 100),
+ repeat("a\n", 100) + "q\nr\ns\nt" + repeat("\nz", 100),
+ Joiner.on('\n')
+ .join(
+ "@@ -98,10 +98,10 @@",
+ " a",
+ " a",
+ " a",
+ "-m",
+ "-n",
+ "-o",
+ "-p",
+ "+q",
+ "+r",
+ "+s",
+ "+t",
+ " z",
+ " z",
+ " z"));
+ }
+
+ @GwtIncompatible
+ @Test
+ public void formatDiffOmitBothMultipleDifferingLinesDifferentLength() {
+ runFormatTest(
+ repeat("a\n", 100) + "m\nn\no\np" + repeat("\nz", 100),
+ repeat("a\n", 100) + "q\nr\ns\nt\nu\nv" + repeat("\nz", 100),
+ Joiner.on('\n')
+ .join(
+ "@@ -98,10 +98,12 @@",
+ " a",
+ " a",
+ " a",
+ "-m",
+ "-n",
+ "-o",
+ "-p",
+ "+q",
+ "+r",
+ "+s",
+ "+t",
+ "+u",
+ "+v",
+ " z",
+ " z",
+ " z"));
+ }
+
+ @GwtIncompatible
+ @Test
+ public void formatDiffPrefixAndSuffixWouldOverlapSimple() {
+ runFormatTest(
+ repeat("a\n", 40) + "l\nm\nn\no\np\n" + repeat("a\n", 40),
+ repeat("a\n", 40) + "l\nm\nn\no\np\nl\nm\nn\no\np\n" + repeat("a\n", 40),
+ Joiner.on('\n')
+ .join(
+ "@@ -43,6 +43,11 @@",
+ " n",
+ " o",
+ " p",
+ "+l",
+ "+m",
+ "+n",
+ "+o",
+ "+p",
+ " a",
+ " a",
+ " a"));
+ }
+
+ @GwtIncompatible
+ @Test
+ public void formatDiffPrefixAndSuffixWouldOverlapAllSame() {
+ runFormatTest(
+ repeat("a\n", 80),
+ repeat("a\n", 82),
+ Joiner.on('\n').join("@@ -78,4 +78,6 @@", " a", " a", " a", "+a", "+a", " "));
+ /*
+ * The final blank line here is odd, and it's different than what Unix diff produces. Maybe look
+ * into removing it if we can do so safely?
+ */
+ }
+
+ @GwtIncompatible
+ @Test
+ public void formatDiffSameExceptNewlineStyle() {
+ runFormatTest(
+ repeat("a\n", 10),
+ repeat("a\r\n", 10),
+ "(line contents match, but line-break characters differ)");
+ }
+
+ @GwtIncompatible
+ @Test
+ public void formatDiffSameExceptTrailingNewline() {
+ runFormatTest(
+ repeat("a\n", 19) + "a",
+ repeat("a\n", 19) + "a\n",
+ Joiner.on('\n').join("@@ -18,3 +18,4 @@", " a", " a", " a", "+"));
+ }
+
+ @GwtIncompatible
+ @Test
+ public void testSerialization_ComparisonFailureWithFacts() {
+ ImmutableList<String> messages = ImmutableList.of("hello");
+ ImmutableList<Fact> facts = ImmutableList.of(fact("first", "value"), simpleFact("second"));
+ String expected = "expected";
+ String actual = "actual";
+ Throwable cause = new Throwable("cause");
+ ComparisonFailureWithFacts original =
+ new ComparisonFailureWithFacts(messages, facts, expected, actual, cause);
+
+ ComparisonFailureWithFacts reserialized = reserialize(original);
+ assertThat(reserialized).hasMessageThat().isEqualTo(original.getMessage());
+ assertThat(reserialized).hasCauseThat().hasMessageThat().isEqualTo(cause.getMessage());
+ assertThat(reserialized.facts().get(0).key).isEqualTo("first");
+ assertThat(reserialized.facts().get(0).value).isEqualTo("value");
+ assertThat(reserialized.facts().get(1).key).isEqualTo("second");
+ assertThat(reserialized.getExpected()).isEqualTo("expected");
+ assertThat(reserialized.getActual()).isEqualTo("actual");
+ }
+
+ @GwtIncompatible
+ @Test
+ public void testSerialization_AssertionErrorWithFacts() {
+ ImmutableList<String> messages = ImmutableList.of("hello");
+ ImmutableList<Fact> facts = ImmutableList.of(fact("first", "value"), simpleFact("second"));
+ Throwable cause = new Throwable("cause");
+ AssertionErrorWithFacts original = new AssertionErrorWithFacts(messages, facts, cause);
+
+ AssertionErrorWithFacts reserialized = reserialize(original);
+ assertThat(reserialized).hasMessageThat().isEqualTo(original.getMessage());
+ assertThat(reserialized).hasCauseThat().hasMessageThat().isEqualTo(cause.getMessage());
+ assertThat(reserialized.facts().get(0).key).isEqualTo("first");
+ assertThat(reserialized.facts().get(0).value).isEqualTo("value");
+ assertThat(reserialized.facts().get(1).key).isEqualTo("second");
+ }
+
+ @GwtIncompatible
+ @Test
+ public void testSerialization_Fact() {
+ Fact original = fact("first", "value");
+ Fact reserialized = reserialize(original);
+ assertThat(reserialized.key).isEqualTo(original.key);
+ assertThat(reserialized.value).isEqualTo(original.value);
+
+ original = simpleFact("second");
+ reserialized = reserialize(original);
+ assertThat(reserialized.key).isEqualTo(original.key);
+ assertThat(reserialized.value).isEqualTo(original.value);
+ }
+
+ private static void runFormatTest(
+ String expected, String actual, String expectedExpected, String expectedActual) {
+ ImmutableList<Fact> facts = formatExpectedAndActual(expected, actual);
+ assertThat(facts).hasSize(2);
+ assertThat(facts.get(0).key).isEqualTo("expected");
+ assertThat(facts.get(1).key).isEqualTo("but was");
+ assertThat(facts.get(0).value).isEqualTo(expectedExpected);
+ assertThat(facts.get(1).value).isEqualTo(expectedActual);
+ }
+
+ @GwtIncompatible
+ private static void runFormatTest(String expected, String actual, String expectedDiff) {
+ ImmutableList<Fact> facts = formatExpectedAndActual(expected, actual);
+ assertThat(facts).hasSize(1);
+ assertThat(facts.get(0).key).isEqualTo("diff (-expected +actual)");
+ assertThat(facts.get(0).value).isEqualTo(expectedDiff);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/CorrespondenceExceptionStoreTest.java b/core/src/test/java/com/google/common/truth/CorrespondenceExceptionStoreTest.java
new file mode 100644
index 00000000..d6589216
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/CorrespondenceExceptionStoreTest.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2019 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.Iterables;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link Correspondence.ExceptionStore}.
+ *
+ * <p>These should not be run under j2cl, because the descriptions don't include the expected stack
+ * traces there.
+ *
+ * @author Pete Gillin
+ */
+@RunWith(JUnit4.class)
+public final class CorrespondenceExceptionStoreTest extends BaseSubjectTestCase {
+
+ @Test
+ public void hasCompareException_empty() {
+ Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forIterable();
+ assertThat(exceptions.hasCompareException()).isFalse();
+ }
+
+ @Test
+ public void hasCompareException_hasCompareException() {
+ Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forIterable();
+ addCompareException(exceptions);
+ assertThat(exceptions.hasCompareException()).isTrue();
+ }
+
+ @Test
+ public void describeAsMainCause_empty() {
+ Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forIterable();
+ try {
+ exceptions.describeAsMainCause();
+ fail("Expected IllegalStateException");
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void describeAsMainCause_notEmpty() {
+ Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forIterable();
+ addCompareException(exceptions);
+ assertExpectedFacts(
+ exceptions.describeAsMainCause(),
+ "one or more exceptions were thrown while comparing elements");
+ }
+
+ @Test
+ public void describeAsAdditionalInfo_empty() {
+ Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forIterable();
+ assertThat(exceptions.describeAsAdditionalInfo()).isEmpty();
+ }
+
+ @Test
+ public void describeAsAdditionalInfo_notEmpty() {
+ Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forIterable();
+ addCompareException(exceptions);
+ assertExpectedFacts(
+ exceptions.describeAsAdditionalInfo(),
+ "additionally, one or more exceptions were thrown while comparing elements");
+ }
+
+ /** Adds a somewhat realistic exception from {@link Correspondence#compare} to the given store. */
+ private static void addCompareException(Correspondence.ExceptionStore exceptions) {
+ try {
+ boolean unused = TestCorrespondences.WITHIN_10_OF.compare(null, 123);
+ } catch (RuntimeException e) {
+ exceptions.addCompareException(CorrespondenceExceptionStoreTest.class, e, null, 123);
+ }
+ }
+
+ /**
+ * Asserts that the given iterable has two facts, the first with the given key and no value, the
+ * second with a key of {@code "first exception"} and a value describing the exception added by
+ * {@link #addCompareException}.
+ */
+ private static void assertExpectedFacts(Iterable<Fact> facts, String expectedFirstKey) {
+ assertThat(facts).hasSize(2);
+ Fact first = Iterables.get(facts, 0);
+ Fact second = Iterables.get(facts, 1);
+ assertThat(first.key).isEqualTo(expectedFirstKey);
+ assertThat(first.value).isNull();
+ assertThat(second.key).isEqualTo("first exception");
+ assertThat(second.value)
+ .matches( // an initial statement of the method that threw and the exception type:
+ "compare\\(null, 123\\) threw java.lang.NullPointerException"
+ // some whitespace:
+ + "\\s+"
+ // the start of a stack trace, with the correct class:
+ + "at com\\.google\\.common\\.truth\\.TestCorrespondences"
+ // the rest of the stack trace, which we don't validate (and may contain newlines):
+ + "(.|\\n)*"
+ // the expected separator
+ + "\\n---");
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/CorrespondenceTest.java b/core/src/test/java/com/google/common/truth/CorrespondenceTest.java
new file mode 100644
index 00000000..ac29057e
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/CorrespondenceTest.java
@@ -0,0 +1,666 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Correspondence.equality;
+import static com.google.common.truth.Correspondence.tolerance;
+import static com.google.common.truth.TestCorrespondences.INT_DIFF_FORMATTER;
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.Arrays.asList;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link Correspondence}.
+ *
+ * @author Pete Gillin
+ */
+@RunWith(JUnit4.class)
+public final class CorrespondenceTest extends BaseSubjectTestCase {
+ // Tests of the abstract base class (just assert that equals and hashCode throw).
+
+ private static final Correspondence<Object, Object> INSTANCE =
+ Correspondence.from(
+ // If we were allowed to use lambdas, this would be:
+ // (a, e) -> false,
+ new Correspondence.BinaryPredicate<Object, Object>() {
+ @Override
+ public boolean apply(@Nullable Object actual, @Nullable Object expected) {
+ return false;
+ }
+ },
+ "has example property");
+
+ @Test
+ @SuppressWarnings("deprecation") // testing deprecated method
+ public void testEquals_throws() {
+ try {
+ INSTANCE.equals(new Object());
+ fail("Expected UnsupportedOperationException from Correspondence.equals");
+ } catch (UnsupportedOperationException expected) {
+ }
+ }
+
+ @Test
+ @SuppressWarnings("deprecation") // testing deprecated method
+ public void testHashCode_throws() {
+ try {
+ INSTANCE.hashCode();
+ fail("Expected UnsupportedOperationException from Correspondence.hashCode");
+ } catch (UnsupportedOperationException expected) {
+ }
+ }
+
+ // Tests of the 'from' factory method.
+
+ private static final Correspondence<String, String> STRING_PREFIX_EQUALITY =
+ // If we were allowed to use method references here, this would be:
+ // Correspondence.from(String::startsWith, "starts with");
+ Correspondence.from(
+ new Correspondence.BinaryPredicate<String, String>() {
+ @Override
+ public boolean apply(String actual, String expected) {
+ return actual.startsWith(expected);
+ }
+ },
+ "starts with");
+
+ @Test
+ public void testFrom_compare() {
+ assertThat(STRING_PREFIX_EQUALITY.compare("foot", "foo")).isTrue();
+ assertThat(STRING_PREFIX_EQUALITY.compare("foot", "foot")).isTrue();
+ assertThat(STRING_PREFIX_EQUALITY.compare("foo", "foot")).isFalse();
+ }
+
+ @Test
+ public void testFrom_formatDiff() {
+ assertThat(STRING_PREFIX_EQUALITY.formatDiff("foo", "foot")).isNull();
+ }
+
+ @Test
+ public void testFrom_toString() {
+ assertThat(STRING_PREFIX_EQUALITY.toString()).isEqualTo("starts with");
+ }
+
+ @Test
+ public void testFrom_isEquality() {
+ assertThat(STRING_PREFIX_EQUALITY.isEquality()).isFalse();
+ }
+
+ @Test
+ public void testFrom_viaIterableSubjectContainsExactly_success() {
+ assertThat(ImmutableList.of("foot", "barn"))
+ .comparingElementsUsing(STRING_PREFIX_EQUALITY)
+ .containsExactly("foo", "bar");
+ }
+
+ @Test
+ public void testFrom_viaIterableSubjectContainsExactly_failure() {
+ expectFailure
+ .whenTesting()
+ .that(ImmutableList.of("foot", "barn", "gallon"))
+ .comparingElementsUsing(STRING_PREFIX_EQUALITY)
+ .containsExactly("foo", "bar");
+ assertFailureKeys("unexpected (1)", "---", "expected", "testing whether", "but was");
+ assertFailureValue("unexpected (1)", "gallon");
+ assertFailureValue("testing whether", "actual element starts with expected element");
+ }
+
+ @Test
+ public void testFrom_viaIterableSubjectContainsExactly_null() {
+ expectFailure
+ .whenTesting()
+ .that(asList("foot", "barn", null))
+ .comparingElementsUsing(STRING_PREFIX_EQUALITY)
+ .containsExactly("foo", "bar");
+ assertFailureKeys(
+ "unexpected (1)",
+ "---",
+ "expected",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertFailureValue("unexpected (1)", "null");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, foo) threw java.lang.NullPointerException");
+ }
+
+ // Tests of the 'transform' factory methods.
+
+ private static final Correspondence<String, Integer> LENGTHS =
+ // If we were allowed to use method references here, this would be:
+ // Correspondence.transforming(String::length, "has a length of");
+ Correspondence.transforming(
+ new Function<String, Integer>() {
+ @Override
+ public Integer apply(String str) {
+ return str.length();
+ }
+ },
+ "has a length of");
+
+ private static final Correspondence<String, Integer> HYPHEN_INDEXES =
+ // If we were allowed to use lambdas here, this would be:
+ // Correspondence.transforming(
+ // str -> {
+ // int index = str.indexOf('-');
+ // return (index >= 0) ? index : null;
+ // },
+ // "has a hyphen at an index of");
+ // (Or else perhaps we'd pull out a method for the lambda body and use a method reference?)
+ Correspondence.transforming(
+ new Function<String, Integer>() {
+ @Override
+ public @Nullable Integer apply(String str) {
+ int index = str.indexOf('-');
+ return (index >= 0) ? index : null;
+ }
+ },
+ "has a hyphen at an index of");
+
+ @Test
+ public void testTransforming_actual_compare() {
+ assertThat(LENGTHS.compare("foo", 3)).isTrue();
+ assertThat(LENGTHS.compare("foot", 4)).isTrue();
+ assertThat(LENGTHS.compare("foo", 4)).isFalse();
+ }
+
+ @Test
+ public void testTransforming_actual_compare_nullTransformedValues() {
+ assertThat(HYPHEN_INDEXES.compare("mailing-list", null)).isFalse();
+ assertThat(HYPHEN_INDEXES.compare("forum", 7)).isFalse();
+ assertThat(HYPHEN_INDEXES.compare("forum", null)).isTrue();
+ }
+
+ @Test
+ public void testTransforming_actual_compare_nullActualValue() {
+ try {
+ HYPHEN_INDEXES.compare(null, 7);
+ fail("Expected NullPointerException to be thrown but wasn't");
+ } catch (NullPointerException expected) {
+ }
+ }
+
+ @Test
+ public void testTransforming_actual_formatDiff() {
+ assertThat(LENGTHS.formatDiff("foo", 4)).isNull();
+ }
+
+ @Test
+ public void testTransforming_actual_toString() {
+ assertThat(LENGTHS.toString()).isEqualTo("has a length of");
+ }
+
+ @Test
+ public void testTransforming_actual_isEquality() {
+ assertThat(LENGTHS.isEquality()).isFalse();
+ }
+
+ @Test
+ public void testTransforming_actual_viaIterableSubjectContainsExactly_success() {
+ assertThat(ImmutableList.of("feet", "barns", "gallons"))
+ .comparingElementsUsing(LENGTHS)
+ .containsExactly(4, 5, 7)
+ .inOrder();
+ }
+
+ @Test
+ public void testTransforming_actual_viaIterableSubjectContainsExactly_failure() {
+ expectFailure
+ .whenTesting()
+ .that(ImmutableList.of("feet", "barns", "gallons"))
+ .comparingElementsUsing(LENGTHS)
+ .containsExactly(4, 5);
+ assertFailureKeys("unexpected (1)", "---", "expected", "testing whether", "but was");
+ assertFailureValue("unexpected (1)", "gallons");
+ assertFailureValue("testing whether", "actual element has a length of expected element");
+ }
+
+ @Test
+ public void testTransforming_actual_viaIterableSubjectContainsExactly_nullActual() {
+ expectFailure
+ .whenTesting()
+ .that(asList("feet", "barns", null))
+ .comparingElementsUsing(LENGTHS)
+ .containsExactly(4, 5);
+ assertFailureKeys(
+ "unexpected (1)",
+ "---",
+ "expected",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertFailureValue("unexpected (1)", "null");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, 4) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void testTransforming_actual_viaIterableSubjectContainsExactly_nullTransformed() {
+ // "mailing-list" and "chat-room" have hyphens at index 7 and 4 respectively.
+ // "forum" contains no hyphen so the Function in HYPHEN_INDEXES transforms it to null.
+ assertThat(ImmutableList.of("mailing-list", "chat-room", "forum"))
+ .comparingElementsUsing(HYPHEN_INDEXES)
+ .containsExactly(7, 4, null)
+ .inOrder();
+ }
+
+ private static final Correspondence<String, String> HYPHENS_MATCH_COLONS =
+ // If we were allowed to use lambdas here, this would be:
+ // Correspondence.transforming(
+ // str -> {
+ // int index = str.indexOf('-');
+ // return (index >= 0) ? index : null;
+ // },
+ // str -> {
+ // int index = str.indexOf(':');
+ // return (index >= 0) ? index : null;
+ // },
+ // "has a hyphen at the same index as the colon in");
+ // (Or else perhaps we'd pull out a method for the lambda bodies?)
+ Correspondence.transforming(
+ new Function<String, Integer>() {
+ @Override
+ public @Nullable Integer apply(String str) {
+ int index = str.indexOf('-');
+ return (index >= 0) ? index : null;
+ }
+ },
+ new Function<String, Integer>() {
+ @Override
+ public @Nullable Integer apply(String str) {
+ int index = str.indexOf(':');
+ return (index >= 0) ? index : null;
+ }
+ },
+ "has a hyphen at the same index as the colon in");
+
+ @Test
+ public void testTransforming_both_compare() {
+ assertThat(HYPHENS_MATCH_COLONS.compare("mailing-list", "abcdefg:hij")).isTrue();
+ assertThat(HYPHENS_MATCH_COLONS.compare("chat-room", "abcd:efghij")).isTrue();
+ assertThat(HYPHENS_MATCH_COLONS.compare("chat-room", "abcdefg:hij")).isFalse();
+ }
+
+ @Test
+ public void testTransforming_both_compare_nullTransformedValue() {
+ assertThat(HYPHENS_MATCH_COLONS.compare("mailing-list", "abcdefg-hij")).isFalse();
+ assertThat(HYPHENS_MATCH_COLONS.compare("forum", "abcde:fghij")).isFalse();
+ assertThat(HYPHENS_MATCH_COLONS.compare("forum", "abcde-fghij")).isTrue();
+ }
+
+ @Test
+ public void testTransforming_both_compare_nullInputValues() {
+ try {
+ HYPHENS_MATCH_COLONS.compare(null, "abcde:fghij");
+ fail("Expected NullPointerException to be thrown but wasn't");
+ } catch (NullPointerException expected) {
+ }
+ try {
+ HYPHENS_MATCH_COLONS.compare("mailing-list", null);
+ fail("Expected NullPointerException to be thrown but wasn't");
+ } catch (NullPointerException expected) {
+ }
+ }
+
+ @Test
+ public void testTransforming_both_formatDiff() {
+ assertThat(HYPHENS_MATCH_COLONS.formatDiff("chat-room", "abcdefg:hij")).isNull();
+ }
+
+ @Test
+ public void testTransforming_both_toString() {
+ assertThat(HYPHENS_MATCH_COLONS.toString())
+ .isEqualTo("has a hyphen at the same index as the colon in");
+ }
+
+ @Test
+ public void testTransforming_both_isEquality() {
+ assertThat(HYPHENS_MATCH_COLONS.isEquality()).isFalse();
+ }
+
+ @Test
+ public void testTransforming_both_viaIterableSubjectContainsExactly_success() {
+ assertThat(ImmutableList.of("mailing-list", "chat-room", "web-app"))
+ .comparingElementsUsing(HYPHENS_MATCH_COLONS)
+ .containsExactly("abcdefg:hij", "abcd:efghij", "abc:defghij")
+ .inOrder();
+ }
+
+ @Test
+ public void testTransforming_both_viaIterableSubjectContainsExactly_failure() {
+ expectFailure
+ .whenTesting()
+ .that(ImmutableList.of("mailing-list", "chat-room", "web-app"))
+ .comparingElementsUsing(HYPHENS_MATCH_COLONS)
+ .containsExactly("abcdefg:hij", "abcd:efghij");
+ assertFailureKeys("unexpected (1)", "---", "expected", "testing whether", "but was");
+ assertFailureValue("unexpected (1)", "web-app");
+ assertFailureValue(
+ "testing whether",
+ "actual element has a hyphen at the same index as the colon in expected element");
+ }
+
+ @Test
+ public void testTransforming_both_viaIterableSubjectContainsExactly_nullActual() {
+ expectFailure
+ .whenTesting()
+ .that(asList("mailing-list", "chat-room", null))
+ .comparingElementsUsing(HYPHENS_MATCH_COLONS)
+ .containsExactly("abcdefg:hij", "abcd:efghij");
+ assertFailureKeys(
+ "unexpected (1)",
+ "---",
+ "expected",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertFailureValue("unexpected (1)", "null");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, abcdefg:hij) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void testTransforming_both_viaIterableSubjectContainsExactly_nullExpected() {
+ expectFailure
+ .whenTesting()
+ .that(ImmutableList.of("mailing-list", "chat-room"))
+ .comparingElementsUsing(HYPHENS_MATCH_COLONS)
+ .containsExactly("abcdefg:hij", "abcd:efghij", null);
+ assertFailureKeys(
+ "missing (1)",
+ "---",
+ "expected",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertFailureValue("missing (1)", "null");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(mailing-list, null) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void testTransforming_both_viaIterableSubjectContainsExactly_nullTransformed() {
+ // The actual element "forum" contains no hyphen, and the expected element "abcde-fghij"
+ // contains no colon, so they both transform to null, and so they correspond.
+ assertThat(ImmutableList.of("mailing-list", "chat-room", "forum"))
+ .comparingElementsUsing(HYPHENS_MATCH_COLONS)
+ .containsExactly("abcdefg:hij", "abcd:efghij", "abcde-fghij")
+ .inOrder();
+ }
+
+ // Tests of the 'tolerance' factory method. Includes both direct tests of the compare method and
+ // indirect tests using it in a basic call chain.
+
+ @Test
+ public void testTolerance_compare_doubles() {
+ assertThat(tolerance(0.0).compare(2.0, 2.0)).isTrue();
+ assertThat(tolerance(0.00001).compare(2.0, 2.0)).isTrue();
+ assertThat(tolerance(1000.0).compare(2.0, 2.0)).isTrue();
+ assertThat(tolerance(1.00001).compare(2.0, 3.0)).isTrue();
+ assertThat(tolerance(1000.0).compare(2.0, 1003.0)).isFalse();
+ assertThat(tolerance(1000.0).compare(2.0, Double.POSITIVE_INFINITY)).isFalse();
+ assertThat(tolerance(1000.0).compare(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY))
+ .isFalse();
+ assertThat(tolerance(1000.0).compare(2.0, Double.NaN)).isFalse();
+ assertThat(tolerance(1000.0).compare(Double.NaN, Double.NaN)).isFalse();
+ assertThat(tolerance(0.0).compare(-0.0, 0.0)).isTrue();
+ }
+
+ @Test
+ public void testTolerance_compare_floats() {
+ assertThat(tolerance(0.0).compare(2.0f, 2.0f)).isTrue();
+ assertThat(tolerance(0.00001).compare(2.0f, 2.0f)).isTrue();
+ assertThat(tolerance(1000.0).compare(2.0f, 2.0f)).isTrue();
+ assertThat(tolerance(1.00001).compare(2.0f, 3.0f)).isTrue();
+ assertThat(tolerance(1000.0).compare(2.0f, 1003.0f)).isFalse();
+ assertThat(tolerance(1000.0).compare(2.0f, Float.POSITIVE_INFINITY)).isFalse();
+ assertThat(tolerance(1000.0).compare(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY))
+ .isFalse();
+ assertThat(tolerance(1000.0).compare(2.0f, Float.NaN)).isFalse();
+ assertThat(tolerance(1000.0).compare(Float.NaN, Float.NaN)).isFalse();
+ assertThat(tolerance(0.0).compare(-0.0f, 0.0f)).isTrue();
+ }
+
+ @Test
+ public void testTolerance_compare_doublesVsInts() {
+ assertThat(tolerance(0.0).compare(2.0, 2)).isTrue();
+ assertThat(tolerance(0.00001).compare(2.0, 2)).isTrue();
+ assertThat(tolerance(1000.0).compare(2.0, 2)).isTrue();
+ assertThat(tolerance(1.00001).compare(2.0, 3)).isTrue();
+ assertThat(tolerance(1000.0).compare(2.0, 1003)).isFalse();
+ }
+
+ @Test
+ public void testTolerance_compare_negativeTolerance() {
+ try {
+ tolerance(-0.05).compare(1.0, 2.0);
+ fail("Expected IllegalArgumentException to be thrown but wasn't");
+ } catch (IllegalArgumentException expected) {
+ assertThat(expected).hasMessageThat().isEqualTo("tolerance (-0.05) cannot be negative");
+ }
+ }
+
+ @Test
+ public void testTolerance_compare_null() {
+ try {
+ tolerance(0.05).compare(1.0, null);
+ fail("Expected NullPointerException to be thrown but wasn't");
+ } catch (NullPointerException expected) {
+ }
+ try {
+ tolerance(0.05).compare(null, 2.0);
+ fail("Expected NullPointerException to be thrown but wasn't");
+ } catch (NullPointerException expected) {
+ }
+ }
+
+ @Test
+ public void testTolerance_formatDiff() {
+ assertThat(tolerance(0.01).formatDiff(1.0, 2.0)).isNull();
+ }
+
+ @Test
+ public void testTolerance_toString() {
+ assertThat(tolerance(0.01).toString()).isEqualTo("is a finite number within 0.01 of");
+ }
+
+ @Test
+ public void testTolerance_isEquality() {
+ assertThat(tolerance(0.01).isEquality()).isFalse();
+ // This is close to equality, but not close enough (it calls numbers of different types equal):
+ assertThat(tolerance(0.0).isEquality()).isFalse();
+ }
+
+ @Test
+ public void testTolerance_viaIterableSubjectContains_success() {
+ assertThat(ImmutableList.of(1.02, 2.04, 3.08))
+ .comparingElementsUsing(tolerance(0.05))
+ .contains(2.0);
+ }
+
+ @Test
+ public void testTolerance_viaIterableSubjectContains_failure() {
+ expectFailure
+ .whenTesting()
+ .that(ImmutableList.of(1.02, 2.04, 3.08))
+ .comparingElementsUsing(tolerance(0.05))
+ .contains(3.01);
+ assertFailureKeys("expected to contain", "testing whether", "but was");
+ assertFailureValue("expected to contain", "3.01");
+ assertFailureValue(
+ "testing whether", "actual element is a finite number within 0.05 of expected element");
+ assertFailureValue("but was", "[1.02, 2.04, 3.08]");
+ }
+
+ // Tests of the 'equality' factory method. Includes both direct tests of the compare method and
+ // indirect tests using it in a basic call chain.
+
+ @Test
+ public void testEquality_compare() {
+ assertThat(equality().compare("foo", "foo")).isTrue();
+ assertThat(equality().compare("foo", "bar")).isFalse();
+ assertThat(equality().compare(123, 123)).isTrue();
+ assertThat(equality().compare(123, 123L)).isFalse();
+ assertThat(equality().compare(null, null)).isTrue();
+ assertThat(equality().compare(null, "bar")).isFalse();
+ }
+
+ @Test
+ public void testEquality_formatDiff() {
+ assertThat(equality().formatDiff("foo", "bar")).isNull();
+ }
+
+ @Test
+ public void testEquality_toString() {
+ assertThat(equality().toString()).isEqualTo("is equal to"); // meta!
+ }
+
+ @Test
+ public void testEquality_isEquality() {
+ assertThat(equality().isEquality()).isTrue();
+ }
+
+ @Test
+ public void testEquality_viaIterableSubjectContains_success() {
+ assertThat(ImmutableList.of(1.0, 2.0, 3.0)).comparingElementsUsing(equality()).contains(2.0);
+ }
+
+ @Test
+ public void testEquality_viaIterableSubjectContains_failure() {
+ expectFailure
+ .whenTesting()
+ .that(ImmutableList.of(1.01, 2.02, 3.03))
+ .comparingElementsUsing(equality())
+ .contains(2.0);
+ // N.B. No "testing whether" fact:
+ assertFailureKeys("expected to contain", "but was");
+ }
+
+ // Tests of formattingDiffsUsing.
+
+ private static final Correspondence<String, Integer> LENGTHS_WITH_DIFF =
+ // If we were allowed to use method references and lambdas here, this would be:
+ // Correspondence.transforming(String::length, "has a length of")
+ // .formattingDiffsUsing((a, e) -> Integer.toString(a.length() - e));
+ Correspondence.transforming(
+ new Function<String, Integer>() {
+ @Override
+ public Integer apply(String str) {
+ return str.length();
+ }
+ },
+ "has a length of")
+ .formattingDiffsUsing(
+ new Correspondence.DiffFormatter<String, Integer>() {
+ @Override
+ public String formatDiff(String actualString, Integer expectedLength) {
+ return Integer.toString(actualString.length() - expectedLength);
+ }
+ });
+
+ @Test
+ public void testFormattingDiffsUsing_compare() {
+ // The compare behaviour should be the same as the wrapped correspondence.
+ assertThat(LENGTHS_WITH_DIFF.compare("foo", 3)).isTrue();
+ assertThat(LENGTHS_WITH_DIFF.compare("foot", 4)).isTrue();
+ assertThat(LENGTHS_WITH_DIFF.compare("foo", 4)).isFalse();
+ }
+
+ @Test
+ public void testFormattingDiffsUsing_formatDiff() {
+ assertThat(LENGTHS_WITH_DIFF.formatDiff("foo", 4)).isEqualTo("-1");
+ assertThat(LENGTHS_WITH_DIFF.formatDiff("foot", 3)).isEqualTo("1");
+ }
+
+ @Test
+ public void testFormattingDiffsUsing_toString() {
+ // The toString behaviour should be the same as the wrapped correspondence.
+ assertThat(LENGTHS_WITH_DIFF.toString()).isEqualTo("has a length of");
+ }
+
+ @Test
+ public void testFormattingDiffsUsing_isEquality() {
+ // The isEquality behaviour should be the same as the wrapped correspondence.
+ assertThat(LENGTHS_WITH_DIFF.isEquality()).isFalse();
+ Correspondence<Integer, Integer> equalityWithDiffFormatter =
+ Correspondence.<Integer>equality().formattingDiffsUsing(INT_DIFF_FORMATTER);
+ assertThat(equalityWithDiffFormatter.isEquality()).isTrue();
+ }
+
+ @Test
+ public void testFormattingDiffsUsing_viaIterableSubjectContainsExactly_failure() {
+ expectFailure
+ .whenTesting()
+ .that(ImmutableList.of("feet", "gallons"))
+ .comparingElementsUsing(LENGTHS_WITH_DIFF)
+ .containsExactly(4, 5);
+ assertFailureKeys(
+ "missing (1)",
+ "unexpected (1)",
+ "#1",
+ "diff",
+ "---",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValue("missing (1)", "5");
+ assertFailureValue("#1", "gallons");
+ assertFailureValue("diff", "2");
+ }
+
+ @Test
+ public void testFormattingDiffsUsing_viaIterableSubjectContainsExactly_nullActual() {
+ expectFailure
+ .whenTesting()
+ .that(asList("feet", null))
+ .comparingElementsUsing(LENGTHS_WITH_DIFF)
+ .containsExactly(4, 5);
+ assertFailureKeys(
+ "missing (1)",
+ "unexpected (1)",
+ "---",
+ "expected",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception",
+ "additionally, one or more exceptions were thrown while formatting diffs",
+ "first exception");
+ assertFailureValue("missing (1)", "5");
+ assertFailureValue("unexpected (1)", "[null]");
+ assertThatFailure()
+ .factValue("first exception", 0)
+ .startsWith("compare(null, 4) threw java.lang.NullPointerException");
+ assertThatFailure()
+ .factValue("first exception", 1)
+ .startsWith("formatDiff(null, 5) threw java.lang.NullPointerException");
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/CustomFailureMessageTest.java b/core/src/test/java/com/google/common/truth/CustomFailureMessageTest.java
new file mode 100644
index 00000000..39910b32
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/CustomFailureMessageTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.common.truth.Truth.assert_;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests (and effectively sample code) for custom error message for checks.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+@RunWith(JUnit4.class)
+public class CustomFailureMessageTest extends BaseSubjectTestCase {
+
+ @Test
+ public void assertWithMessageThat() {
+ expectFailure.whenTesting().withMessage("This is a custom message").that(false).isTrue();
+ assertThat(expectFailure.getFailure())
+ .hasMessageThat()
+ .startsWith("This is a custom message\n");
+ }
+
+ @Test
+ public void countPlaceholders() {
+ assertThat(LazyMessage.countPlaceholders("")).isEqualTo(0);
+ assertThat(LazyMessage.countPlaceholders("%s")).isEqualTo(1);
+ assertThat(LazyMessage.countPlaceholders("%s%s")).isEqualTo(2);
+ assertThat(LazyMessage.countPlaceholders("%s%%s")).isEqualTo(2);
+ assertThat(LazyMessage.countPlaceholders("hello")).isEqualTo(0);
+ assertThat(LazyMessage.countPlaceholders("%shello")).isEqualTo(1);
+ assertThat(LazyMessage.countPlaceholders("hello%s")).isEqualTo(1);
+ assertThat(LazyMessage.countPlaceholders("hel%slo")).isEqualTo(1);
+ assertThat(LazyMessage.countPlaceholders("hel%%slo")).isEqualTo(1);
+ assertThat(LazyMessage.countPlaceholders("hel%s%slo")).isEqualTo(2);
+ assertThat(LazyMessage.countPlaceholders("%shel%s%slo")).isEqualTo(3);
+ assertThat(LazyMessage.countPlaceholders("hel%s%slo%s")).isEqualTo(3);
+ }
+
+ @Test
+ public void assertWithMessageThat_withPlaceholders() {
+ expectFailure
+ .whenTesting()
+ .withMessage("This is a %s %s", "custom", "message")
+ .that(false)
+ .isTrue();
+ assertThat(expectFailure.getFailure())
+ .hasMessageThat()
+ .startsWith("This is a custom message\n");
+ }
+
+ @Test
+ public void extraPlaceholderThrowsIae() {
+ try {
+ assert_().withMessage("This is a %s %s", "custom").that(true).isTrue();
+ fail("Should have thrown");
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test
+ public void missingPlaceholderThrowsIae() {
+ try {
+ assert_().withMessage("This is a %s", "custom", "message").that(true).isTrue();
+ fail("Should have thrown");
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test
+ public void noPlaceholdersWithArgsThrowsIae() {
+ try {
+ assert_().withMessage("This is a custom message", "bad arg").that(true).isTrue();
+ fail("Should have thrown");
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test
+ public void placeholdersArentEagerlyEvaluated() {
+ Object toStringThrows =
+ new Object() {
+ @Override
+ public String toString() {
+ throw new RuntimeException("Don't call me!");
+ }
+ };
+ assertWithMessage("Evaluating this will blow up: %s", toStringThrows).that(true).isTrue();
+ assert_().withMessage("Evaluating this will blow up: %s", toStringThrows).that(true).isTrue();
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/DoubleSubjectTest.java b/core/src/test/java/com/google/common/truth/DoubleSubjectTest.java
new file mode 100644
index 00000000..f96b474e
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/DoubleSubjectTest.java
@@ -0,0 +1,596 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.ExpectFailure.assertThat;
+import static com.google.common.truth.Platform.doubleToString;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.annotations.GwtIncompatible;
+import com.google.common.truth.ExpectFailure.SimpleSubjectBuilderCallback;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Double Subjects.
+ *
+ * @author Kurt Alfred Kluever
+ */
+@RunWith(JUnit4.class)
+public class DoubleSubjectTest extends BaseSubjectTestCase {
+
+ private static final double NEARLY_MAX = 1.7976931348623155E308;
+ private static final double NEGATIVE_NEARLY_MAX = -1.7976931348623155E308;
+ private static final double OVER_MIN = 1.0E-323;
+ private static final double UNDER_NEGATIVE_MIN = -1.0E-323;
+ private static final double GOLDEN = 1.23;
+ private static final double OVER_GOLDEN = 1.2300000000000002;
+
+ private static final Subject.Factory<DoubleSubject, Double> DOUBLE_SUBJECT_FACTORY =
+ new Subject.Factory<DoubleSubject, Double>() {
+ @Override
+ public DoubleSubject createSubject(FailureMetadata metadata, Double that) {
+ return new DoubleSubject(metadata, that);
+ }
+ };
+
+ @CanIgnoreReturnValue
+ private static AssertionError expectFailure(
+ SimpleSubjectBuilderCallback<DoubleSubject, Double> callback) {
+ return ExpectFailure.expectFailureAbout(DOUBLE_SUBJECT_FACTORY, callback);
+ }
+
+ @Test
+ @GwtIncompatible("Math.nextAfter")
+ public void testDoubleConstants_matchNextAfter() {
+ assertThat(Math.nextAfter(Double.MIN_VALUE, 1.0)).isEqualTo(OVER_MIN);
+ assertThat(Math.nextAfter(1.23, Double.POSITIVE_INFINITY)).isEqualTo(OVER_GOLDEN);
+ assertThat(Math.nextAfter(Double.MAX_VALUE, 0.0)).isEqualTo(NEARLY_MAX);
+ assertThat(Math.nextAfter(-1.0 * Double.MAX_VALUE, 0.0)).isEqualTo(NEGATIVE_NEARLY_MAX);
+ assertThat(Math.nextAfter(-1.0 * Double.MIN_VALUE, -1.0)).isEqualTo(UNDER_NEGATIVE_MIN);
+ }
+
+ @Test
+ public void testJ2clCornerCaseZero() {
+ // GWT considers -0.0 to be equal to 0.0. But we've added a special workaround inside Truth.
+ assertThatIsEqualToFails(-0.0, 0.0);
+ }
+
+ @Test
+ @GwtIncompatible("GWT behavior difference")
+ public void testJ2clCornerCaseDoubleVsFloat() {
+ // Under GWT, 1.23f.toString() is different than 1.23d.toString(), so the message omits types.
+ // TODO(b/35377736): Consider making Truth add the types anyway.
+ expectFailureWhenTestingThat(1.23).isEqualTo(1.23f);
+ assertFailureKeys("expected", "an instance of", "but was", "an instance of");
+ }
+
+ @Test
+ public void isWithinOf() {
+ assertThat(2.0).isWithin(0.0).of(2.0);
+ assertThat(2.0).isWithin(0.00001).of(2.0);
+ assertThat(2.0).isWithin(1000.0).of(2.0);
+ assertThat(2.0).isWithin(1.00001).of(3.0);
+ assertThatIsWithinFails(2.0, 0.99999, 3.0);
+ assertThatIsWithinFails(2.0, 1000.0, 1003.0);
+ assertThatIsWithinFails(2.0, 1000.0, Double.POSITIVE_INFINITY);
+ assertThatIsWithinFails(2.0, 1000.0, Double.NaN);
+ assertThatIsWithinFails(Double.NEGATIVE_INFINITY, 1000.0, 2.0);
+ assertThatIsWithinFails(Double.NaN, 1000.0, 2.0);
+ }
+
+ private static void assertThatIsWithinFails(
+ final double actual, final double tolerance, final double expected) {
+ ExpectFailure.SimpleSubjectBuilderCallback<DoubleSubject, Double> callback =
+ new ExpectFailure.SimpleSubjectBuilderCallback<DoubleSubject, Double>() {
+ @Override
+ public void invokeAssertion(SimpleSubjectBuilder<DoubleSubject, Double> expect) {
+ expect.that(actual).isWithin(tolerance).of(expected);
+ }
+ };
+ AssertionError failure = expectFailure(callback);
+ assertThat(failure)
+ .factKeys()
+ .containsExactly("expected", "but was", "outside tolerance")
+ .inOrder();
+ assertThat(failure).factValue("expected").isEqualTo(doubleToString(expected));
+ assertThat(failure).factValue("but was").isEqualTo(doubleToString(actual));
+ assertThat(failure).factValue("outside tolerance").isEqualTo(doubleToString(tolerance));
+ }
+
+ @Test
+ public void isNotWithinOf() {
+ assertThatIsNotWithinFails(2.0, 0.0, 2.0);
+ assertThatIsNotWithinFails(2.0, 0.00001, 2.0);
+ assertThatIsNotWithinFails(2.0, 1000.0, 2.0);
+ assertThatIsNotWithinFails(2.0, 1.00001, 3.0);
+ assertThat(2.0).isNotWithin(0.99999).of(3.0);
+ assertThat(2.0).isNotWithin(1000.0).of(1003.0);
+ assertThatIsNotWithinFails(2.0, 0.0, Double.POSITIVE_INFINITY);
+ assertThatIsNotWithinFails(2.0, 0.0, Double.NaN);
+ assertThatIsNotWithinFails(Double.NEGATIVE_INFINITY, 1000.0, 2.0);
+ assertThatIsNotWithinFails(Double.NaN, 1000.0, 2.0);
+ }
+
+ private static void assertThatIsNotWithinFails(
+ final double actual, final double tolerance, final double expected) {
+ ExpectFailure.SimpleSubjectBuilderCallback<DoubleSubject, Double> callback =
+ new ExpectFailure.SimpleSubjectBuilderCallback<DoubleSubject, Double>() {
+ @Override
+ public void invokeAssertion(SimpleSubjectBuilder<DoubleSubject, Double> expect) {
+ expect.that(actual).isNotWithin(tolerance).of(expected);
+ }
+ };
+ AssertionError failure = expectFailure(callback);
+ assertThat(failure).factValue("expected not to be").isEqualTo(doubleToString(expected));
+ assertThat(failure).factValue("within tolerance").isEqualTo(doubleToString(tolerance));
+ }
+
+ @Test
+ public void negativeTolerances() {
+ isWithinNegativeToleranceThrowsIAE(5.0, -0.5, 4.9);
+ isWithinNegativeToleranceThrowsIAE(5.0, -0.5, 4.0);
+
+ isNotWithinNegativeToleranceThrowsIAE(5.0, -0.5, 4.9);
+ isNotWithinNegativeToleranceThrowsIAE(5.0, -0.5, 4.0);
+
+ isWithinNegativeToleranceThrowsIAE(+0.0, -0.00001, +0.0);
+ isWithinNegativeToleranceThrowsIAE(+0.0, -0.00001, -0.0);
+ isWithinNegativeToleranceThrowsIAE(-0.0, -0.00001, +0.0);
+ isWithinNegativeToleranceThrowsIAE(-0.0, -0.00001, -0.0);
+
+ isNotWithinNegativeToleranceThrowsIAE(+0.0, -0.00001, +1.0);
+ isNotWithinNegativeToleranceThrowsIAE(+0.0, -0.00001, -1.0);
+ isNotWithinNegativeToleranceThrowsIAE(-0.0, -0.00001, +1.0);
+ isNotWithinNegativeToleranceThrowsIAE(-0.0, -0.00001, -1.0);
+
+ isNotWithinNegativeToleranceThrowsIAE(+1.0, -0.00001, +0.0);
+ isNotWithinNegativeToleranceThrowsIAE(+1.0, -0.00001, -0.0);
+ isNotWithinNegativeToleranceThrowsIAE(-1.0, -0.00001, +0.0);
+ isNotWithinNegativeToleranceThrowsIAE(-1.0, -0.00001, -0.0);
+
+ // You know what's worse than zero? Negative zero.
+
+ isWithinNegativeToleranceThrowsIAE(+0.0, -0.0, +0.0);
+ isWithinNegativeToleranceThrowsIAE(+0.0, -0.0, -0.0);
+ isWithinNegativeToleranceThrowsIAE(-0.0, -0.0, +0.0);
+ isWithinNegativeToleranceThrowsIAE(-0.0, -0.0, -0.0);
+
+ isNotWithinNegativeToleranceThrowsIAE(+1.0, -0.0, +0.0);
+ isNotWithinNegativeToleranceThrowsIAE(+1.0, -0.0, -0.0);
+ isNotWithinNegativeToleranceThrowsIAE(-1.0, -0.0, +0.0);
+ isNotWithinNegativeToleranceThrowsIAE(-1.0, -0.0, -0.0);
+ }
+
+ private static void isWithinNegativeToleranceThrowsIAE(
+ double actual, double tolerance, double expected) {
+ try {
+ assertThat(actual).isWithin(tolerance).of(expected);
+ fail("Expected IllegalArgumentException to be thrown but wasn't");
+ } catch (IllegalArgumentException iae) {
+ assertThat(iae)
+ .hasMessageThat()
+ .isEqualTo("tolerance (" + tolerance + ") cannot be negative");
+ }
+ }
+
+ private static void isNotWithinNegativeToleranceThrowsIAE(
+ double actual, double tolerance, double expected) {
+ try {
+ assertThat(actual).isNotWithin(tolerance).of(expected);
+ fail("Expected IllegalArgumentException to be thrown but wasn't");
+ } catch (IllegalArgumentException iae) {
+ assertThat(iae)
+ .hasMessageThat()
+ .isEqualTo("tolerance (" + tolerance + ") cannot be negative");
+ }
+ }
+
+ @Test
+ public void nanTolerances() {
+ try {
+ assertThat(1.0).isWithin(Double.NaN).of(1.0);
+ fail("Expected IllegalArgumentException to be thrown but wasn't");
+ } catch (IllegalArgumentException iae) {
+ assertThat(iae).hasMessageThat().isEqualTo("tolerance cannot be NaN");
+ }
+ try {
+ assertThat(1.0).isNotWithin(Double.NaN).of(2.0);
+ fail("Expected IllegalArgumentException to be thrown but wasn't");
+ } catch (IllegalArgumentException iae) {
+ assertThat(iae).hasMessageThat().isEqualTo("tolerance cannot be NaN");
+ }
+ }
+
+ @Test
+ public void infiniteTolerances() {
+ try {
+ assertThat(1.0).isWithin(Double.POSITIVE_INFINITY).of(1.0);
+ fail("Expected IllegalArgumentException to be thrown but wasn't");
+ } catch (IllegalArgumentException iae) {
+ assertThat(iae).hasMessageThat().isEqualTo("tolerance cannot be POSITIVE_INFINITY");
+ }
+ try {
+ assertThat(1.0).isNotWithin(Double.POSITIVE_INFINITY).of(2.0);
+ fail("Expected IllegalArgumentException to be thrown but wasn't");
+ } catch (IllegalArgumentException iae) {
+ assertThat(iae).hasMessageThat().isEqualTo("tolerance cannot be POSITIVE_INFINITY");
+ }
+ }
+
+ @Test
+ public void isWithinOfZero() {
+ assertThat(+0.0).isWithin(0.00001).of(+0.0);
+ assertThat(+0.0).isWithin(0.00001).of(-0.0);
+ assertThat(-0.0).isWithin(0.00001).of(+0.0);
+ assertThat(-0.0).isWithin(0.00001).of(-0.0);
+
+ assertThat(+0.0).isWithin(0.0).of(+0.0);
+ assertThat(+0.0).isWithin(0.0).of(-0.0);
+ assertThat(-0.0).isWithin(0.0).of(+0.0);
+ assertThat(-0.0).isWithin(0.0).of(-0.0);
+ }
+
+ @Test
+ public void isNotWithinOfZero() {
+ assertThat(+0.0).isNotWithin(0.00001).of(+1.0);
+ assertThat(+0.0).isNotWithin(0.00001).of(-1.0);
+ assertThat(-0.0).isNotWithin(0.00001).of(+1.0);
+ assertThat(-0.0).isNotWithin(0.00001).of(-1.0);
+
+ assertThat(+1.0).isNotWithin(0.00001).of(+0.0);
+ assertThat(+1.0).isNotWithin(0.00001).of(-0.0);
+ assertThat(-1.0).isNotWithin(0.00001).of(+0.0);
+ assertThat(-1.0).isNotWithin(0.00001).of(-0.0);
+
+ assertThat(+1.0).isNotWithin(0.0).of(+0.0);
+ assertThat(+1.0).isNotWithin(0.0).of(-0.0);
+ assertThat(-1.0).isNotWithin(0.0).of(+0.0);
+ assertThat(-1.0).isNotWithin(0.0).of(-0.0);
+
+ assertThatIsNotWithinFails(-0.0, 0.0, 0.0);
+ }
+
+ @Test
+ public void isWithinZeroTolerance() {
+ double max = Double.MAX_VALUE;
+ assertThat(max).isWithin(0.0).of(max);
+ assertThat(NEARLY_MAX).isWithin(0.0).of(NEARLY_MAX);
+ assertThatIsWithinFails(max, 0.0, NEARLY_MAX);
+ assertThatIsWithinFails(NEARLY_MAX, 0.0, max);
+
+ double negativeMax = -1.0 * Double.MAX_VALUE;
+ assertThat(negativeMax).isWithin(0.0).of(negativeMax);
+ assertThat(NEGATIVE_NEARLY_MAX).isWithin(0.0).of(NEGATIVE_NEARLY_MAX);
+ assertThatIsWithinFails(negativeMax, 0.0, NEGATIVE_NEARLY_MAX);
+ assertThatIsWithinFails(NEGATIVE_NEARLY_MAX, 0.0, negativeMax);
+
+ double min = Double.MIN_VALUE;
+ assertThat(min).isWithin(0.0).of(min);
+ assertThat(OVER_MIN).isWithin(0.0).of(OVER_MIN);
+ assertThatIsWithinFails(min, 0.0, OVER_MIN);
+ assertThatIsWithinFails(OVER_MIN, 0.0, min);
+
+ double negativeMin = -1.0 * Double.MIN_VALUE;
+ assertThat(negativeMin).isWithin(0.0).of(negativeMin);
+ assertThat(UNDER_NEGATIVE_MIN).isWithin(0.0).of(UNDER_NEGATIVE_MIN);
+ assertThatIsWithinFails(negativeMin, 0.0, UNDER_NEGATIVE_MIN);
+ assertThatIsWithinFails(UNDER_NEGATIVE_MIN, 0.0, negativeMin);
+ }
+
+ @Test
+ public void isNotWithinZeroTolerance() {
+ double max = Double.MAX_VALUE;
+ assertThatIsNotWithinFails(max, 0.0, max);
+ assertThatIsNotWithinFails(NEARLY_MAX, 0.0, NEARLY_MAX);
+ assertThat(max).isNotWithin(0.0).of(NEARLY_MAX);
+ assertThat(NEARLY_MAX).isNotWithin(0.0).of(max);
+
+ double min = Double.MIN_VALUE;
+ assertThatIsNotWithinFails(min, 0.0, min);
+ assertThatIsNotWithinFails(OVER_MIN, 0.0, OVER_MIN);
+ assertThat(min).isNotWithin(0.0).of(OVER_MIN);
+ assertThat(OVER_MIN).isNotWithin(0.0).of(min);
+ }
+
+ @Test
+ public void isWithinNonFinite() {
+ assertThatIsWithinFails(Double.NaN, 0.00001, Double.NaN);
+ assertThatIsWithinFails(Double.NaN, 0.00001, Double.POSITIVE_INFINITY);
+ assertThatIsWithinFails(Double.NaN, 0.00001, Double.NEGATIVE_INFINITY);
+ assertThatIsWithinFails(Double.NaN, 0.00001, +0.0);
+ assertThatIsWithinFails(Double.NaN, 0.00001, -0.0);
+ assertThatIsWithinFails(Double.NaN, 0.00001, +1.0);
+ assertThatIsWithinFails(Double.NaN, 0.00001, -0.0);
+ assertThatIsWithinFails(Double.POSITIVE_INFINITY, 0.00001, Double.POSITIVE_INFINITY);
+ assertThatIsWithinFails(Double.POSITIVE_INFINITY, 0.00001, Double.NEGATIVE_INFINITY);
+ assertThatIsWithinFails(Double.POSITIVE_INFINITY, 0.00001, +0.0);
+ assertThatIsWithinFails(Double.POSITIVE_INFINITY, 0.00001, -0.0);
+ assertThatIsWithinFails(Double.POSITIVE_INFINITY, 0.00001, +1.0);
+ assertThatIsWithinFails(Double.POSITIVE_INFINITY, 0.00001, -0.0);
+ assertThatIsWithinFails(Double.NEGATIVE_INFINITY, 0.00001, Double.NEGATIVE_INFINITY);
+ assertThatIsWithinFails(Double.NEGATIVE_INFINITY, 0.00001, +0.0);
+ assertThatIsWithinFails(Double.NEGATIVE_INFINITY, 0.00001, -0.0);
+ assertThatIsWithinFails(Double.NEGATIVE_INFINITY, 0.00001, +1.0);
+ assertThatIsWithinFails(Double.NEGATIVE_INFINITY, 0.00001, -0.0);
+ assertThatIsWithinFails(+1.0, 0.00001, Double.NaN);
+ assertThatIsWithinFails(+1.0, 0.00001, Double.POSITIVE_INFINITY);
+ assertThatIsWithinFails(+1.0, 0.00001, Double.NEGATIVE_INFINITY);
+ }
+
+ @Test
+ public void isNotWithinNonFinite() {
+ assertThatIsNotWithinFails(Double.NaN, 0.00001, Double.NaN);
+ assertThatIsNotWithinFails(Double.NaN, 0.00001, Double.POSITIVE_INFINITY);
+ assertThatIsNotWithinFails(Double.NaN, 0.00001, Double.NEGATIVE_INFINITY);
+ assertThatIsNotWithinFails(Double.NaN, 0.00001, +0.0);
+ assertThatIsNotWithinFails(Double.NaN, 0.00001, -0.0);
+ assertThatIsNotWithinFails(Double.NaN, 0.00001, +1.0);
+ assertThatIsNotWithinFails(Double.NaN, 0.00001, -0.0);
+ assertThatIsNotWithinFails(Double.POSITIVE_INFINITY, 0.00001, Double.POSITIVE_INFINITY);
+ assertThatIsNotWithinFails(Double.POSITIVE_INFINITY, 0.00001, Double.NEGATIVE_INFINITY);
+ assertThatIsNotWithinFails(Double.POSITIVE_INFINITY, 0.00001, +0.0);
+ assertThatIsNotWithinFails(Double.POSITIVE_INFINITY, 0.00001, -0.0);
+ assertThatIsNotWithinFails(Double.POSITIVE_INFINITY, 0.00001, +1.0);
+ assertThatIsNotWithinFails(Double.POSITIVE_INFINITY, 0.00001, -0.0);
+ assertThatIsNotWithinFails(Double.NEGATIVE_INFINITY, 0.00001, Double.NEGATIVE_INFINITY);
+ assertThatIsNotWithinFails(Double.NEGATIVE_INFINITY, 0.00001, +0.0);
+ assertThatIsNotWithinFails(Double.NEGATIVE_INFINITY, 0.00001, -0.0);
+ assertThatIsNotWithinFails(Double.NEGATIVE_INFINITY, 0.00001, +1.0);
+ assertThatIsNotWithinFails(Double.NEGATIVE_INFINITY, 0.00001, -0.0);
+ assertThatIsNotWithinFails(+1.0, 0.00001, Double.NaN);
+ assertThatIsNotWithinFails(+1.0, 0.00001, Double.POSITIVE_INFINITY);
+ assertThatIsNotWithinFails(+1.0, 0.00001, Double.NEGATIVE_INFINITY);
+ }
+
+ @SuppressWarnings("TruthSelfEquals")
+ @Test
+ public void isEqualTo() {
+ assertThat(1.23).isEqualTo(1.23);
+ assertThatIsEqualToFails(GOLDEN, OVER_GOLDEN);
+ assertThat(Double.POSITIVE_INFINITY).isEqualTo(Double.POSITIVE_INFINITY);
+ assertThat(Double.NaN).isEqualTo(Double.NaN);
+ assertThat((Double) null).isEqualTo(null);
+ assertThat(1.0).isEqualTo(1);
+ }
+
+ private static void assertThatIsEqualToFails(final double actual, final double expected) {
+ ExpectFailure.SimpleSubjectBuilderCallback<DoubleSubject, Double> callback =
+ new ExpectFailure.SimpleSubjectBuilderCallback<DoubleSubject, Double>() {
+ @Override
+ public void invokeAssertion(SimpleSubjectBuilder<DoubleSubject, Double> expect) {
+ expect.that(actual).isEqualTo(expected);
+ }
+ };
+ expectFailure(callback);
+ }
+
+ @Test
+ public void isNotEqualTo() {
+ assertThatIsNotEqualToFails(1.23);
+ assertThat(GOLDEN).isNotEqualTo(OVER_GOLDEN);
+ assertThatIsNotEqualToFails(Double.POSITIVE_INFINITY);
+ assertThatIsNotEqualToFails(Double.NaN);
+ assertThat(-0.0).isNotEqualTo(0.0);
+ assertThatIsNotEqualToFails(null);
+ assertThat(1.23).isNotEqualTo(1.23f);
+ assertThat(1.0).isNotEqualTo(2);
+ }
+
+ private static void assertThatIsNotEqualToFails(final @Nullable Double value) {
+ ExpectFailure.SimpleSubjectBuilderCallback<DoubleSubject, Double> callback =
+ new ExpectFailure.SimpleSubjectBuilderCallback<DoubleSubject, Double>() {
+ @Override
+ public void invokeAssertion(SimpleSubjectBuilder<DoubleSubject, Double> expect) {
+ expect.that(value).isNotEqualTo(value);
+ }
+ };
+ expectFailure(callback);
+ }
+
+ @Test
+ public void isZero() {
+ assertThat(0.0).isZero();
+ assertThat(-0.0).isZero();
+ assertThatIsZeroFails(Double.MIN_VALUE);
+ assertThatIsZeroFails(-1.23);
+ assertThatIsZeroFails(Double.POSITIVE_INFINITY);
+ assertThatIsZeroFails(Double.NaN);
+ assertThatIsZeroFails(null);
+ }
+
+ private static void assertThatIsZeroFails(final @Nullable Double value) {
+ ExpectFailure.SimpleSubjectBuilderCallback<DoubleSubject, Double> callback =
+ new ExpectFailure.SimpleSubjectBuilderCallback<DoubleSubject, Double>() {
+ @Override
+ public void invokeAssertion(SimpleSubjectBuilder<DoubleSubject, Double> expect) {
+ expect.that(value).isZero();
+ }
+ };
+ AssertionError failure = expectFailure(callback);
+ assertThat(failure).factKeys().containsExactly("expected zero", "but was").inOrder();
+ }
+
+ @Test
+ public void isNonZero() {
+ assertThatIsNonZeroFails(0.0, "expected not to be zero");
+ assertThatIsNonZeroFails(-0.0, "expected not to be zero");
+ assertThat(Double.MIN_VALUE).isNonZero();
+ assertThat(-1.23).isNonZero();
+ assertThat(Double.POSITIVE_INFINITY).isNonZero();
+ assertThat(Double.NaN).isNonZero();
+ assertThatIsNonZeroFails(null, "expected a double other than zero");
+ }
+
+ private static void assertThatIsNonZeroFails(final @Nullable Double value, String factKey) {
+ ExpectFailure.SimpleSubjectBuilderCallback<DoubleSubject, Double> callback =
+ new ExpectFailure.SimpleSubjectBuilderCallback<DoubleSubject, Double>() {
+ @Override
+ public void invokeAssertion(SimpleSubjectBuilder<DoubleSubject, Double> expect) {
+ expect.that(value).isNonZero();
+ }
+ };
+ AssertionError failure = expectFailure(callback);
+ assertThat(failure).factKeys().containsExactly(factKey, "but was").inOrder();
+ }
+
+ @Test
+ public void isPositiveInfinity() {
+ assertThat(Double.POSITIVE_INFINITY).isPositiveInfinity();
+ assertThatIsPositiveInfinityFails(1.23);
+ assertThatIsPositiveInfinityFails(Double.NEGATIVE_INFINITY);
+ assertThatIsPositiveInfinityFails(Double.NaN);
+ assertThatIsPositiveInfinityFails(null);
+ }
+
+ private static void assertThatIsPositiveInfinityFails(final @Nullable Double value) {
+ ExpectFailure.SimpleSubjectBuilderCallback<DoubleSubject, Double> callback =
+ new ExpectFailure.SimpleSubjectBuilderCallback<DoubleSubject, Double>() {
+ @Override
+ public void invokeAssertion(SimpleSubjectBuilder<DoubleSubject, Double> expect) {
+ expect.that(value).isPositiveInfinity();
+ }
+ };
+ expectFailure(callback);
+ }
+
+ @Test
+ public void isNegativeInfinity() {
+ assertThat(Double.NEGATIVE_INFINITY).isNegativeInfinity();
+ assertThatIsNegativeInfinityFails(1.23);
+ assertThatIsNegativeInfinityFails(Double.POSITIVE_INFINITY);
+ assertThatIsNegativeInfinityFails(Double.NaN);
+ assertThatIsNegativeInfinityFails(null);
+ }
+
+ private static void assertThatIsNegativeInfinityFails(final @Nullable Double value) {
+ ExpectFailure.SimpleSubjectBuilderCallback<DoubleSubject, Double> callback =
+ new ExpectFailure.SimpleSubjectBuilderCallback<DoubleSubject, Double>() {
+ @Override
+ public void invokeAssertion(SimpleSubjectBuilder<DoubleSubject, Double> expect) {
+ expect.that(value).isNegativeInfinity();
+ }
+ };
+ expectFailure(callback);
+ }
+
+ @Test
+ public void isNaN() {
+ assertThat(Double.NaN).isNaN();
+ assertThatIsNaNFails(1.23);
+ assertThatIsNaNFails(Double.POSITIVE_INFINITY);
+ assertThatIsNaNFails(Double.NEGATIVE_INFINITY);
+ assertThatIsNaNFails(null);
+ }
+
+ private static void assertThatIsNaNFails(final @Nullable Double value) {
+ ExpectFailure.SimpleSubjectBuilderCallback<DoubleSubject, Double> callback =
+ new ExpectFailure.SimpleSubjectBuilderCallback<DoubleSubject, Double>() {
+ @Override
+ public void invokeAssertion(SimpleSubjectBuilder<DoubleSubject, Double> expect) {
+ expect.that(value).isNaN();
+ }
+ };
+ expectFailure(callback);
+ }
+
+ @Test
+ public void isFinite() {
+ assertThat(1.23).isFinite();
+ assertThat(Double.MAX_VALUE).isFinite();
+ assertThat(-1.0 * Double.MIN_VALUE).isFinite();
+ assertThatIsFiniteFails(Double.POSITIVE_INFINITY);
+ assertThatIsFiniteFails(Double.NEGATIVE_INFINITY);
+ assertThatIsFiniteFails(Double.NaN);
+ assertThatIsFiniteFails(null);
+ }
+
+ private static void assertThatIsFiniteFails(final @Nullable Double value) {
+ ExpectFailure.SimpleSubjectBuilderCallback<DoubleSubject, Double> callback =
+ new ExpectFailure.SimpleSubjectBuilderCallback<DoubleSubject, Double>() {
+ @Override
+ public void invokeAssertion(SimpleSubjectBuilder<DoubleSubject, Double> expect) {
+ expect.that(value).isFinite();
+ }
+ };
+ AssertionError failure = expectFailure(callback);
+ assertThat(failure).factKeys().containsExactly("expected to be finite", "but was").inOrder();
+ }
+
+ @Test
+ public void isNotNaN() {
+ assertThat(1.23).isNotNaN();
+ assertThat(Double.MAX_VALUE).isNotNaN();
+ assertThat(-1.0 * Double.MIN_VALUE).isNotNaN();
+ assertThat(Double.POSITIVE_INFINITY).isNotNaN();
+ assertThat(Double.NEGATIVE_INFINITY).isNotNaN();
+ }
+
+ @Test
+ public void isNotNaNIsNaN() {
+ expectFailureWhenTestingThat(Double.NaN).isNotNaN();
+ }
+
+ @Test
+ public void isNotNaNIsNull() {
+ expectFailureWhenTestingThat(null).isNotNaN();
+ assertFailureKeys("expected a double other than NaN", "but was");
+ }
+
+ @Test
+ public void isGreaterThan_int_strictly() {
+ expectFailureWhenTestingThat(2.0).isGreaterThan(3);
+ }
+
+ @Test
+ public void isGreaterThan_int() {
+ expectFailureWhenTestingThat(2.0).isGreaterThan(2);
+ assertThat(2.0).isGreaterThan(1);
+ }
+
+ @Test
+ public void isLessThan_int_strictly() {
+ expectFailureWhenTestingThat(2.0).isLessThan(1);
+ }
+
+ @Test
+ public void isLessThan_int() {
+ expectFailureWhenTestingThat(2.0).isLessThan(2);
+ assertThat(2.0).isLessThan(3);
+ }
+
+ @Test
+ public void isAtLeast_int() {
+ expectFailureWhenTestingThat(2.0).isAtLeast(3);
+ assertThat(2.0).isAtLeast(2);
+ assertThat(2.0).isAtLeast(1);
+ }
+
+ @Test
+ public void isAtMost_int() {
+ expectFailureWhenTestingThat(2.0).isAtMost(1);
+ assertThat(2.0).isAtMost(2);
+ assertThat(2.0).isAtMost(3);
+ }
+
+ private DoubleSubject expectFailureWhenTestingThat(Double actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/ExpectFailureNonRuleTest.java b/core/src/test/java/com/google/common/truth/ExpectFailureNonRuleTest.java
new file mode 100644
index 00000000..1e8f464d
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/ExpectFailureNonRuleTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Lists;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runner.Runner;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunListener;
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link ExpectFailure} not used as JUnit's TestRule. */
+@RunWith(JUnit4.class)
+public class ExpectFailureNonRuleTest {
+
+ @Test
+ public void testExpect_userThrowExceptionInSubject_shouldPropagate() throws Exception {
+ final List<Failure> reportedFailure = Lists.newArrayList();
+ RunNotifier runNotifier = new RunNotifier();
+ runNotifier.addListener(
+ new RunListener() {
+ @Override
+ public void testFailure(Failure failure) throws Exception {
+ reportedFailure.add(failure);
+ }
+ });
+
+ Runner runner = new JUnit4(ExpectFailureThrowInSubject.class);
+ runner.run(runNotifier);
+
+ assertThat(reportedFailure).hasSize(2);
+ assertThat(reportedFailure.get(0).getException())
+ .hasMessageThat()
+ .contains("Throw deliberately");
+ assertThat(reportedFailure.get(1).getException())
+ .hasMessageThat()
+ .contains("ExpectFailure.whenTesting() invoked, but no failure was caught.");
+ }
+
+ @Test
+ public void testExpect_userThrowExceptionAfterSubject_shouldPropagate() throws Exception {
+ final List<Failure> reportedFailure = Lists.newArrayList();
+ RunNotifier runNotifier = new RunNotifier();
+ runNotifier.addListener(
+ new RunListener() {
+ @Override
+ public void testFailure(Failure failure) throws Exception {
+ reportedFailure.add(failure);
+ }
+ });
+
+ Runner runner = new JUnit4(ExpectFailureThrowAfterSubject.class);
+ runner.run(runNotifier);
+
+ assertThat(reportedFailure).hasSize(2);
+ assertThat(reportedFailure.get(0).getException())
+ .hasMessageThat()
+ .contains("Throw deliberately");
+ assertThat(reportedFailure.get(1).getException())
+ .hasMessageThat()
+ .contains("ExpectFailure.whenTesting() invoked, but no failure was caught.");
+ }
+
+ /**
+ * A test supporting test class which will fail because method in a subject will throw exception.
+ */
+ public static class ExpectFailureThrowInSubject {
+
+ final ExpectFailure expectFailure = new ExpectFailure();
+
+ @Before
+ public void setupExpectFailure() {
+ expectFailure.enterRuleContext();
+ }
+
+ @After
+ public void ensureFailureCaught() {
+ expectFailure.ensureFailureCaught();
+ expectFailure.leaveRuleContext();
+ }
+
+ @Test
+ public void testExpect_throwInSubject_shouldPropagate() {
+ expectFailure.whenTesting().that(throwingMethod()).isEqualTo(4);
+ }
+ }
+
+ /**
+ * A test supporting test class which will fail because method after a subject will throw
+ * exception.
+ */
+ public static class ExpectFailureThrowAfterSubject {
+
+ final ExpectFailure expectFailure = new ExpectFailure();
+
+ @Before
+ public void setupExpectFailure() {
+ expectFailure.enterRuleContext();
+ }
+
+ @After
+ public void ensureFailureCaught() {
+ expectFailure.ensureFailureCaught();
+ expectFailure.leaveRuleContext();
+ }
+
+ @Test
+ public void testExpect_throwInSubject_shouldPropagate() {
+ expectFailure.whenTesting().that(4).isEqualTo(4); // No failure being caught
+ long unused = throwingMethod();
+ }
+ }
+
+ private static long throwingMethod() {
+ throw new RuntimeException("Throw deliberately");
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/ExpectFailureRuleTest.java b/core/src/test/java/com/google/common/truth/ExpectFailureRuleTest.java
new file mode 100644
index 00000000..d79d75b7
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/ExpectFailureRuleTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.annotations.GwtIncompatible;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link ExpectFailure} used as JUnit {@link Rule).*/
+@RunWith(JUnit4.class)
+@GwtIncompatible("org.junit.Rule")
+public class ExpectFailureRuleTest {
+ @Rule public final ExpectFailure expectFailure = new ExpectFailure();
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void expectFail_captureFailureAsExpected() {
+ expectFailure.whenTesting().withMessage("abc").fail();
+ assertThat(expectFailure.getFailure()).hasMessageThat().isEqualTo("abc");
+ }
+
+ @Test
+ public void expectFail_passesIfUnused() {
+ assertThat(4).isEqualTo(4);
+ }
+
+ @Test
+ public void expectFail_failsAfterTest() {
+ expectFailure.whenTesting().that(4).isEqualTo(4);
+ thrown.expectMessage("ExpectFailure.whenTesting() invoked, but no failure was caught.");
+ }
+
+ @Test
+ public void expectFail_throwInSubject_shouldPropagateOriginalException() {
+ thrown.expectMessage("Throwing deliberately");
+ expectFailure.whenTesting().that(throwingMethod()).isEqualTo(2);
+ }
+
+ @Test
+ public void expectFail_throwAfterSubject_shouldPropagateOriginalException() {
+ expectFailure.whenTesting().that(2).isEqualTo(2);
+ thrown.expectMessage("Throwing deliberately");
+ throwingMethod();
+ }
+
+ private static long throwingMethod() {
+ throw new RuntimeException("Throwing deliberately");
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/ExpectFailureTest.java b/core/src/test/java/com/google/common/truth/ExpectFailureTest.java
new file mode 100644
index 00000000..74dd80dd
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/ExpectFailureTest.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Strings.lenientFormat;
+import static com.google.common.truth.Fact.simpleFact;
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link ExpectFailure} */
+@RunWith(JUnit4.class)
+public class ExpectFailureTest {
+ private final ExpectFailure expectFailure = new ExpectFailure();
+
+ @Before
+ public void setupExpectFailure() {
+ expectFailure.enterRuleContext();
+ }
+
+ @Test
+ public void expectFail() {
+ expectFailure.whenTesting().withMessage("abc").fail();
+ assertThat(expectFailure.getFailure()).hasMessageThat().isEqualTo("abc");
+ }
+
+ @Test
+ public void expectFail_withCause() {
+ expectFailure.whenTesting().that(new NullPointerException()).isNull();
+ assertThat(expectFailure.getFailure()).hasMessageThat().contains("NullPointerException");
+ assertThat(expectFailure.getFailure()).hasCauseThat().isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ public void expectFail_about() {
+ expectFailure.whenTesting().about(strings()).that("foo").isEqualTo("bar");
+ assertThat(expectFailure.getFailure()).hasMessageThat().contains("foo");
+ }
+
+ @Test
+ public void expectFail_passesIfUnused() {
+ assertThat(4).isEqualTo(4);
+ }
+
+ @Test
+ public void expectFail_failsOnSuccess() {
+ expectFailure.whenTesting().that(4).isEqualTo(4);
+ try {
+ @SuppressWarnings("unused")
+ AssertionError unused = expectFailure.getFailure();
+ throw new Error("Expected to fail");
+ } catch (AssertionError expected) {
+ assertThat(expected).hasMessageThat().contains("ExpectFailure did not capture a failure.");
+ }
+ }
+
+ @Test
+ public void expectFail_failsOnMultipleFailures() {
+ try {
+ expectFailure.whenTesting().about(BadSubject.badSubject()).that(5).isEqualTo(4);
+ throw new Error("Expected to fail");
+ } catch (AssertionError expected) {
+ assertThat(expected).hasMessageThat().contains("caught multiple failures");
+ assertThat(expected).hasMessageThat().contains("<4> is equal to <5>");
+ assertThat(expected).hasMessageThat().contains("<5> is equal to <4>");
+ }
+ }
+
+ @Test
+ public void expectFail_failsOnMultiplewhenTestings() {
+ try {
+ expectFailure.whenTesting().that(4).isEqualTo(4);
+ StandardSubjectBuilder unused = expectFailure.whenTesting();
+ throw new Error("Expected to fail");
+ } catch (AssertionError expected) {
+ assertThat(expected)
+ .hasMessageThat()
+ .contains(
+ "ExpectFailure.whenTesting() called previously, but did not capture a failure.");
+ }
+ }
+
+ @Test
+ public void expectFail_failsOnMultiplewhenTestings_thatFail() {
+ expectFailure.whenTesting().that(5).isEqualTo(4);
+ try {
+ StandardSubjectBuilder unused = expectFailure.whenTesting();
+ throw new Error("Expected to fail");
+ } catch (AssertionError expected) {
+ assertThat(expected).hasMessageThat().contains("ExpectFailure already captured a failure");
+ }
+ }
+
+ @Test
+ public void expectFail_failsAfterTest() {
+ try {
+ expectFailure.whenTesting().that(4).isEqualTo(4);
+ expectFailure.ensureFailureCaught();
+ throw new Error("Expected to fail");
+ } catch (AssertionError expected) {
+ assertThat(expected)
+ .hasMessageThat()
+ .contains("ExpectFailure.whenTesting() invoked, but no failure was caught.");
+ }
+ }
+
+ @Test
+ public void expectFail_whenTestingWithoutInContext_shouldFail() {
+ ExpectFailure expectFailure = new ExpectFailure();
+ try {
+ expectFailure.whenTesting().that(4).isEqualTo(4);
+ throw new Error("Expected to fail");
+ } catch (IllegalStateException expected) {
+ assertThat(expected).hasMessageThat().contains("ExpectFailure must be used as a JUnit @Rule");
+ }
+ }
+
+ private static Subject.Factory<StringSubject, String> strings() {
+ return new Subject.Factory<StringSubject, String>() {
+ @Override
+ public StringSubject createSubject(FailureMetadata fm, String that) {
+ return new StringSubject(fm, that);
+ }
+ };
+ }
+
+ private static class BadSubject extends Subject {
+ private final Integer actual;
+
+ BadSubject(FailureMetadata failureMetadat, Integer actual) {
+ super(failureMetadat, actual);
+ this.actual = actual;
+ }
+
+ @Override
+ public void isEqualTo(Object expected) {
+ if (!actual.equals(expected)) {
+ failWithoutActual(
+ simpleFact(lenientFormat("expected <%s> is equal to <%s>", actual, expected)));
+ failWithoutActual(
+ simpleFact(lenientFormat("expected <%s> is equal to <%s>", expected, actual)));
+ }
+ }
+
+ private static Subject.Factory<BadSubject, Integer> badSubject() {
+ return new Subject.Factory<BadSubject, Integer>() {
+ @Override
+ public BadSubject createSubject(FailureMetadata fm, Integer that) {
+ return new BadSubject(fm, that);
+ }
+ };
+ }
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/ExpectFailureWithStackTraceTest.java b/core/src/test/java/com/google/common/truth/ExpectFailureWithStackTraceTest.java
new file mode 100644
index 00000000..3542475c
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/ExpectFailureWithStackTraceTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2015 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.junit.runners.model.Statement;
+
+/** Test that stack traces are included in the error message created by Expect. */
+@RunWith(JUnit4.class)
+public class ExpectFailureWithStackTraceTest {
+ private static final String METHOD_NAME = "ExpectFailureWithStackTraceTest.expectTwoFailures";
+
+ @Rule public final FailingExpect failToExpect = new FailingExpect();
+
+ @Test
+ public void expectTwoFailures() {
+ failToExpect.delegate.that(4).isNotEqualTo(4);
+ failToExpect.delegate.that("abc").contains("x");
+ }
+
+ /** Expect class that can examine the error message */
+ public static class FailingExpect implements TestRule {
+ final Expect delegate = Expect.create();
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+ final Statement s = delegate.apply(base, description);
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ String failureMessage = "";
+ try {
+ s.evaluate();
+ } catch (AssertionError e) {
+ failureMessage = e.getMessage();
+ }
+ // Check that error message contains stack traces. Method name should appear twice,
+ // once for each expect error.
+ int firstIndex = failureMessage.indexOf(METHOD_NAME);
+ assertThat(firstIndex).isGreaterThan(0);
+ int secondIndex = failureMessage.indexOf(METHOD_NAME, firstIndex + 1);
+ assertThat(secondIndex).isGreaterThan(firstIndex);
+ }
+ };
+ }
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/ExpectTest.java b/core/src/test/java/com/google/common/truth/ExpectTest.java
new file mode 100644
index 00000000..e079ee14
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/ExpectTest.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
+import static com.google.common.util.concurrent.Uninterruptibles.awaitUninterruptibly;
+import static java.util.concurrent.Executors.newFixedThreadPool;
+import static java.util.concurrent.Executors.newSingleThreadExecutor;
+import static org.junit.Assert.fail;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.junit.runners.model.Statement;
+
+/**
+ * Tests (and effectively sample code) for the Expect verb (implemented as a rule)
+ *
+ * @author David Saff
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+@RunWith(JUnit4.class)
+public class ExpectTest {
+ private final Expect oopsNotARule = Expect.create();
+
+ private final Expect expect = Expect.create();
+ private final ExpectedException thrown = ExpectedException.none();
+
+ private final TestRule postTestWait =
+ new TestRule() {
+ @Override
+ public Statement apply(final Statement base, Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ base.evaluate();
+ testMethodComplete.countDown();
+ taskToAwait.get();
+ }
+ };
+ }
+ };
+
+ private final CountDownLatch testMethodComplete = new CountDownLatch(1);
+
+ /**
+ * A task that the main thread will await, to be provided by tests that do work in other threads.
+ */
+ private Future<?> taskToAwait = immediateFuture(null);
+
+ @Rule
+ public final TestRule wrapper =
+ new TestRule() {
+ @Override
+ public Statement apply(Statement statement, Description description) {
+ statement = expect.apply(statement, description);
+ statement = postTestWait.apply(statement, description);
+ statement = thrown.apply(statement, description);
+ return statement;
+ }
+ };
+
+ @Test
+ public void expectTrue() {
+ expect.that(4).isEqualTo(4);
+ }
+
+ @Test
+ public void singleExpectationFails() {
+ thrown.expectMessage("1 expectation failed:");
+ thrown.expectMessage("1. x");
+ expect.withMessage("x").fail();
+ }
+
+ @Test
+ public void expectFail() {
+ thrown.expectMessage("3 expectations failed:");
+ thrown.expectMessage("1. x");
+ thrown.expectMessage("2. y");
+ thrown.expectMessage("3. z");
+ expect.withMessage("x").fail();
+ expect.withMessage("y").fail();
+ expect.withMessage("z").fail();
+ }
+
+ @Test
+ public void expectFail10Aligned() {
+ thrown.expectMessage("10 expectations failed:");
+ thrown.expectMessage(" 1. x");
+ thrown.expectMessage("10. x");
+ for (int i = 0; i < 10; i++) {
+ expect.withMessage("x").fail();
+ }
+ }
+
+ @Test
+ public void expectFail10WrappedAligned() {
+ thrown.expectMessage("10 expectations failed:");
+ thrown.expectMessage(" 1. abc\n xyz");
+ thrown.expectMessage("10. abc\n xyz");
+ for (int i = 0; i < 10; i++) {
+ expect.withMessage("abc\nxyz").fail();
+ }
+ }
+
+ @Test
+ public void expectFailWithExceptionNoMessage() {
+ thrown.expectMessage("3 expectations failed:");
+ thrown.expectMessage("1. x");
+ thrown.expectMessage("2. y");
+ thrown.expectMessage("3. Also, after those failures, an exception was thrown:");
+ expect.withMessage("x").fail();
+ expect.withMessage("y").fail();
+ throw new IllegalStateException();
+ }
+
+ @Test
+ public void expectFailWithExceptionWithMessage() {
+ thrown.expectMessage("3 expectations failed:");
+ thrown.expectMessage("1. x");
+ thrown.expectMessage("2. y");
+ thrown.expectMessage("3. Also, after those failures, an exception was thrown:");
+ expect.withMessage("x").fail();
+ expect.withMessage("y").fail();
+ throw new IllegalStateException("testing");
+ }
+
+ @Test
+ public void expectFailWithExceptionBeforeExpectFailures() {
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("testing");
+ throwException();
+ expect.withMessage("x").fail();
+ expect.withMessage("y").fail();
+ }
+
+ private void throwException() {
+ throw new IllegalStateException("testing");
+ }
+
+ @Test
+ public void expectFailWithFailuresBeforeAssume() {
+ thrown.expectMessage("3 expectations failed:");
+ thrown.expectMessage("1. x");
+ thrown.expectMessage("2. y");
+ thrown.expectMessage("3. Also, after those failures, an assumption was violated:");
+ expect.withMessage("x").fail();
+ expect.withMessage("y").fail();
+ assume().withMessage("testing").fail();
+ }
+
+ @Test
+ public void expectSuccessWithFailuresAfterAssume() {
+ assume().withMessage("testing").fail();
+ expect.withMessage("x").fail();
+ expect.withMessage("y").fail();
+ }
+
+ @Test
+ public void warnWhenExpectIsNotRule() {
+ String message = "assertion made on Expect instance, but it's not enabled as a @Rule.";
+ thrown.expectMessage(message);
+ oopsNotARule.that(true).isEqualTo(true);
+ }
+
+ @Test
+ public void bash() throws Exception {
+ Runnable task =
+ new Runnable() {
+ @Override
+ public void run() {
+ expect.that(3).isEqualTo(4);
+ }
+ };
+ List<Future<?>> results = new ArrayList<>();
+ ExecutorService executor = newFixedThreadPool(10);
+ for (int i = 0; i < 1000; i++) {
+ results.add(executor.submit(task));
+ }
+ executor.shutdown();
+ for (Future<?> result : results) {
+ result.get();
+ }
+ thrown.expectMessage("1000 expectations failed:");
+ }
+
+ @Test
+ public void failWhenCallingThatAfterTest() {
+ ExecutorService executor = newSingleThreadExecutor();
+ taskToAwait =
+ executor.submit(
+ new Runnable() {
+ @Override
+ public void run() {
+ awaitUninterruptibly(testMethodComplete);
+ try {
+ expect.that(3);
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+ });
+ executor.shutdown();
+ }
+
+ @Test
+ public void failWhenCallingFailingAssertionMethodAfterTest() {
+ ExecutorService executor = newSingleThreadExecutor();
+ /*
+ * We wouldn't expect people to do this exactly. The point is that, if someone were to call
+ * expect.that(3).isEqualTo(4), we would always either fail the test or throw an
+ * IllegalStateException, not record a "failure" that we never read.
+ */
+ final IntegerSubject expectThat3 = expect.that(3);
+ taskToAwait =
+ executor.submit(
+ new Runnable() {
+ @Override
+ public void run() {
+ awaitUninterruptibly(testMethodComplete);
+ try {
+ expectThat3.isEqualTo(4);
+ fail();
+ } catch (IllegalStateException expected) {
+ assertThat(expected).hasCauseThat().isInstanceOf(AssertionError.class);
+ }
+ }
+ });
+ executor.shutdown();
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/ExpectWithStackTest.java b/core/src/test/java/com/google/common/truth/ExpectWithStackTest.java
new file mode 100644
index 00000000..528568c8
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/ExpectWithStackTest.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (c) 2018 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.junit.runners.model.Statement;
+
+@RunWith(JUnit4.class)
+public class ExpectWithStackTest {
+ private final Expect expectWithTrace = Expect.create();
+
+ @Rule public final TestRuleVerifier verifyAssertionError = new TestRuleVerifier(expectWithTrace);
+
+ @Test
+ public void testExpectTrace_simpleCase() {
+ verifyAssertionError.setErrorVerifier(
+ new ErrorVerifier() {
+ @Override
+ public void verify(AssertionError expected) {
+ assertThat(expected.getStackTrace()).hasLength(0);
+ assertThat(expected).hasMessageThat().startsWith("3 expectations failed:");
+ }
+ });
+
+ expectWithTrace.that(true).isFalse();
+ expectWithTrace.that("Hello").isNull();
+ expectWithTrace.that(1).isEqualTo(2);
+ }
+
+ @Test
+ public void testExpectTrace_loop() {
+ verifyAssertionError.setErrorVerifier(
+ new ErrorVerifier() {
+ @Override
+ public void verify(AssertionError expected) {
+ assertThat(expected.getStackTrace()).hasLength(0);
+ assertThat(expected).hasMessageThat().startsWith("4 expectations failed:");
+ assertWithMessage("test method name should only show up once with following omitted")
+ .that(expected.getMessage().split("testExpectTrace_loop"))
+ .hasLength(2);
+ }
+ });
+
+ for (int i = 0; i < 4; i++) {
+ expectWithTrace.that(true).isFalse();
+ }
+ }
+
+ @Test
+ public void testExpectTrace_callerException() {
+ verifyAssertionError.setErrorVerifier(
+ new ErrorVerifier() {
+ @Override
+ public void verify(AssertionError expected) {
+ assertThat(expected.getStackTrace()).hasLength(0);
+ assertThat(expected).hasMessageThat().startsWith("2 expectations failed:");
+ }
+ });
+
+ expectWithTrace.that(true).isFalse();
+ expectWithTrace
+ .that(alwaysFailWithCause(getFirstException("First", getSecondException("Second", null))))
+ .isEqualTo(5);
+ }
+
+ @Test
+ public void testExpectTrace_onlyCallerException() {
+ verifyAssertionError.setErrorVerifier(
+ new ErrorVerifier() {
+ @Override
+ public void verify(AssertionError expected) {
+ assertWithMessage("Should throw exception as it is if only caller exception")
+ .that(expected.getStackTrace().length)
+ .isAtLeast(2);
+ }
+ });
+
+ expectWithTrace
+ .that(alwaysFailWithCause(getFirstException("First", getSecondException("Second", null))))
+ .isEqualTo(5);
+ }
+
+ private static long alwaysFailWithCause(Throwable throwable) {
+ throw new AssertionError("Always fail", throwable);
+ }
+
+ private static Exception getFirstException(String message, Throwable cause) {
+ if (cause != null) {
+ return new RuntimeException(message, cause);
+ } else {
+ return new RuntimeException(message);
+ }
+ }
+
+ private static Exception getSecondException(String message, Throwable cause) {
+ if (cause != null) {
+ return new RuntimeException(message, cause);
+ } else {
+ return new RuntimeException(message);
+ }
+ }
+
+ private static final class TestRuleVerifier implements TestRule {
+ private final TestRule ruleToVerify;
+ private ErrorVerifier errorVerifier = NO_VERIFIER;
+
+ TestRuleVerifier(TestRule ruleToVerify) {
+ this.ruleToVerify = ruleToVerify;
+ }
+
+ void setErrorVerifier(ErrorVerifier verifier) {
+ this.errorVerifier = verifier;
+ }
+
+ @Override
+ public Statement apply(final Statement base, final Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ try {
+ ruleToVerify.apply(base, description).evaluate();
+ } catch (AssertionError caught) {
+ errorVerifier.verify(caught);
+ }
+ }
+ };
+ }
+ }
+
+ interface ErrorVerifier {
+ void verify(AssertionError error);
+ }
+
+ private static final ErrorVerifier NO_VERIFIER =
+ new ErrorVerifier() {
+ @Override
+ public void verify(AssertionError expected) {}
+ };
+}
diff --git a/core/src/test/java/com/google/common/truth/FactTest.java b/core/src/test/java/com/google/common/truth/FactTest.java
new file mode 100644
index 00000000..6a928abf
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/FactTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2018 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.Fact.makeMessage;
+import static com.google.common.truth.Fact.simpleFact;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link Fact}. */
+@RunWith(JUnit4.class)
+public class FactTest {
+ @Test
+ public void string() {
+ assertThat(fact("foo", "bar").toString()).isEqualTo("foo: bar");
+ }
+
+ @Test
+ public void stringWithoutValue() {
+ assertThat(simpleFact("foo").toString()).isEqualTo("foo");
+ }
+
+ @Test
+ public void oneFacts() {
+ assertThat(makeMessage(ImmutableList.<String>of(), ImmutableList.of(fact("foo", "bar"))))
+ .isEqualTo("foo: bar");
+ }
+
+ @Test
+ public void twoFacts() {
+ assertThat(
+ makeMessage(
+ ImmutableList.<String>of(),
+ ImmutableList.of(fact("foo", "bar"), fact("longer name", "other value"))))
+ .isEqualTo("foo : bar\nlonger name: other value");
+ }
+
+ @Test
+ public void oneFactWithoutValue() {
+ assertThat(makeMessage(ImmutableList.<String>of(), ImmutableList.of(simpleFact("foo"))))
+ .isEqualTo("foo");
+ }
+
+ @Test
+ public void twoFactsOneWithoutValue() {
+ assertThat(
+ makeMessage(
+ ImmutableList.<String>of(),
+ ImmutableList.of(fact("hello", "there"), simpleFact("foo"))))
+ .isEqualTo("hello: there\nfoo");
+ }
+
+ @Test
+ public void newline() {
+ assertThat(makeMessage(ImmutableList.<String>of(), ImmutableList.of(fact("foo", "bar\nbaz"))))
+ .isEqualTo("foo:\n bar\n baz");
+ }
+
+ @Test
+ public void newlineWithoutValue() {
+ assertThat(
+ makeMessage(
+ ImmutableList.<String>of(),
+ ImmutableList.of(fact("hello", "there\neveryone"), simpleFact("xyz"))))
+ .isEqualTo("hello:\n there\n everyone\nxyz");
+ }
+
+ @Test
+ public void withMessage() {
+ assertThat(makeMessage(ImmutableList.<String>of("hello"), ImmutableList.of(fact("foo", "bar"))))
+ .isEqualTo("hello\nfoo: bar");
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/FloatSubjectTest.java b/core/src/test/java/com/google/common/truth/FloatSubjectTest.java
new file mode 100644
index 00000000..c404a731
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/FloatSubjectTest.java
@@ -0,0 +1,608 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.ExpectFailure.assertThat;
+import static com.google.common.truth.Platform.floatToString;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.annotations.GwtIncompatible;
+import com.google.common.truth.ExpectFailure.SimpleSubjectBuilderCallback;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Float Subjects.
+ *
+ * @author Kurt Alfred Kluever
+ */
+@RunWith(JUnit4.class)
+public class FloatSubjectTest extends BaseSubjectTestCase {
+ private static final float NEARLY_MAX = 3.4028233E38f;
+ private static final float NEGATIVE_NEARLY_MAX = -3.4028233E38f;
+ private static final float JUST_OVER_MIN = 2.8E-45f;
+ private static final float JUST_UNDER_NEGATIVE_MIN = -2.8E-45f;
+ private static final float GOLDEN = 1.23f;
+ private static final float JUST_OVER_GOLDEN = 1.2300001f;
+
+ private static final Subject.Factory<FloatSubject, Float> FLOAT_SUBJECT_FACTORY =
+ new Subject.Factory<FloatSubject, Float>() {
+ @Override
+ public FloatSubject createSubject(FailureMetadata metadata, Float that) {
+ return new FloatSubject(metadata, that);
+ }
+ };
+
+ @CanIgnoreReturnValue
+ private static AssertionError expectFailure(
+ SimpleSubjectBuilderCallback<FloatSubject, Float> callback) {
+ return ExpectFailure.expectFailureAbout(FLOAT_SUBJECT_FACTORY, callback);
+ }
+
+ @Test
+ @GwtIncompatible("Math.nextAfter")
+ public void testFloatConstants_matchNextAfter() {
+ assertThat(Math.nextAfter(Float.MAX_VALUE, 0.0f)).isEqualTo(NEARLY_MAX);
+ assertThat(Math.nextAfter(-1.0f * Float.MAX_VALUE, 0.0f)).isEqualTo(NEGATIVE_NEARLY_MAX);
+ assertThat(Math.nextAfter(Float.MIN_VALUE, 1.0f)).isEqualTo(JUST_OVER_MIN);
+ assertThat(Math.nextAfter(-1.0f * Float.MIN_VALUE, -1.0f)).isEqualTo(JUST_UNDER_NEGATIVE_MIN);
+ assertThat(1.23f).isEqualTo(GOLDEN);
+ assertThat(Math.nextAfter(1.23f, Float.POSITIVE_INFINITY)).isEqualTo(JUST_OVER_GOLDEN);
+ }
+
+ @Test
+ public void testJ2clCornerCaseZero() {
+ // GWT considers -0.0 to be equal to 0.0. But we've added a special workaround inside Truth.
+ assertThatIsEqualToFails(-0.0f, 0.0f);
+ }
+
+ @Test
+ @GwtIncompatible("GWT behavior difference")
+ public void j2clCornerCaseDoubleVsFloat() {
+ // Under GWT, 1.23f.toString() is different than 1.23d.toString(), so the message omits types.
+ // TODO(b/35377736): Consider making Truth add the types manually.
+ expectFailureWhenTestingThat(1.23f).isEqualTo(1.23);
+ assertFailureKeys("expected", "an instance of", "but was", "an instance of");
+ }
+
+ @Test
+ public void isWithinOf() {
+ assertThat(2.0f).isWithin(0.0f).of(2.0f);
+ assertThat(2.0f).isWithin(0.00001f).of(2.0f);
+ assertThat(2.0f).isWithin(1000.0f).of(2.0f);
+ assertThat(2.0f).isWithin(1.00001f).of(3.0f);
+ assertThatIsWithinFails(2.0f, 0.99999f, 3.0f);
+ assertThatIsWithinFails(2.0f, 1000.0f, 1003.0f);
+ assertThatIsWithinFails(2.0f, 1000.0f, Float.POSITIVE_INFINITY);
+ assertThatIsWithinFails(2.0f, 1000.0f, Float.NaN);
+ assertThatIsWithinFails(Float.NEGATIVE_INFINITY, 1000.0f, 2.0f);
+ assertThatIsWithinFails(Float.NaN, 1000.0f, 2.0f);
+ }
+
+ private static void assertThatIsWithinFails(
+ final float actual, final float tolerance, final float expected) {
+ ExpectFailure.SimpleSubjectBuilderCallback<FloatSubject, Float> callback =
+ new ExpectFailure.SimpleSubjectBuilderCallback<FloatSubject, Float>() {
+ @Override
+ public void invokeAssertion(SimpleSubjectBuilder<FloatSubject, Float> expect) {
+ expect.that(actual).isWithin(tolerance).of(expected);
+ }
+ };
+ AssertionError failure = expectFailure(callback);
+ assertThat(failure)
+ .factKeys()
+ .containsExactly("expected", "but was", "outside tolerance")
+ .inOrder();
+ assertThat(failure).factValue("expected").isEqualTo(floatToString(expected));
+ assertThat(failure).factValue("but was").isEqualTo(floatToString(actual));
+ assertThat(failure).factValue("outside tolerance").isEqualTo(floatToString(tolerance));
+ }
+
+ @Test
+ public void isNotWithinOf() {
+ assertThatIsNotWithinFails(2.0f, 0.0f, 2.0f);
+ assertThatIsNotWithinFails(2.0f, 0.00001f, 2.0f);
+ assertThatIsNotWithinFails(2.0f, 1000.0f, 2.0f);
+ assertThatIsNotWithinFails(2.0f, 1.00001f, 3.0f);
+ assertThat(2.0f).isNotWithin(0.99999f).of(3.0f);
+ assertThat(2.0f).isNotWithin(1000.0f).of(1003.0f);
+ assertThatIsNotWithinFails(2.0f, 0.0f, Float.POSITIVE_INFINITY);
+ assertThatIsNotWithinFails(2.0f, 0.0f, Float.NaN);
+ assertThatIsNotWithinFails(Float.NEGATIVE_INFINITY, 1000.0f, 2.0f);
+ assertThatIsNotWithinFails(Float.NaN, 1000.0f, 2.0f);
+ }
+
+ private static void assertThatIsNotWithinFails(
+ final float actual, final float tolerance, final float expected) {
+ ExpectFailure.SimpleSubjectBuilderCallback<FloatSubject, Float> callback =
+ new ExpectFailure.SimpleSubjectBuilderCallback<FloatSubject, Float>() {
+ @Override
+ public void invokeAssertion(SimpleSubjectBuilder<FloatSubject, Float> expect) {
+ expect.that(actual).isNotWithin(tolerance).of(expected);
+ }
+ };
+ AssertionError failure = expectFailure(callback);
+ assertThat(failure).factValue("expected not to be").isEqualTo(floatToString(expected));
+ assertThat(failure).factValue("within tolerance").isEqualTo(floatToString(tolerance));
+ }
+
+ @Test
+ public void negativeTolerances() {
+ isWithinNegativeToleranceThrowsIAE(5.0f, -0.5f, 4.9f);
+ isWithinNegativeToleranceThrowsIAE(5.0f, -0.5f, 4.0f);
+
+ isNotWithinNegativeToleranceThrowsIAE(5.0f, -0.5f, 4.9f);
+ isNotWithinNegativeToleranceThrowsIAE(5.0f, -0.5f, 4.0f);
+
+ isWithinNegativeToleranceThrowsIAE(+0.0f, -0.00001f, +0.0f);
+ isWithinNegativeToleranceThrowsIAE(+0.0f, -0.00001f, -0.0f);
+ isWithinNegativeToleranceThrowsIAE(-0.0f, -0.00001f, +0.0f);
+ isWithinNegativeToleranceThrowsIAE(-0.0f, -0.00001f, -0.0f);
+
+ isNotWithinNegativeToleranceThrowsIAE(+0.0f, -0.00001f, +1.0f);
+ isNotWithinNegativeToleranceThrowsIAE(+0.0f, -0.00001f, -1.0f);
+ isNotWithinNegativeToleranceThrowsIAE(-0.0f, -0.00001f, +1.0f);
+ isNotWithinNegativeToleranceThrowsIAE(-0.0f, -0.00001f, -1.0f);
+
+ isNotWithinNegativeToleranceThrowsIAE(+1.0f, -0.00001f, +0.0f);
+ isNotWithinNegativeToleranceThrowsIAE(+1.0f, -0.00001f, -0.0f);
+ isNotWithinNegativeToleranceThrowsIAE(-1.0f, -0.00001f, +0.0f);
+ isNotWithinNegativeToleranceThrowsIAE(-1.0f, -0.00001f, -0.0f);
+
+ // You know what's worse than zero? Negative zero.
+
+ isWithinNegativeToleranceThrowsIAE(+0.0f, -0.0f, +0.0f);
+ isWithinNegativeToleranceThrowsIAE(+0.0f, -0.0f, -0.0f);
+ isWithinNegativeToleranceThrowsIAE(-0.0f, -0.0f, +0.0f);
+ isWithinNegativeToleranceThrowsIAE(-0.0f, -0.0f, -0.0f);
+
+ isNotWithinNegativeToleranceThrowsIAE(+1.0f, -0.0f, +0.0f);
+ isNotWithinNegativeToleranceThrowsIAE(+1.0f, -0.0f, -0.0f);
+ isNotWithinNegativeToleranceThrowsIAE(-1.0f, -0.0f, +0.0f);
+ isNotWithinNegativeToleranceThrowsIAE(-1.0f, -0.0f, -0.0f);
+ }
+
+ private static void isWithinNegativeToleranceThrowsIAE(
+ float actual, float tolerance, float expected) {
+ try {
+ assertThat(actual).isWithin(tolerance).of(expected);
+ fail("Expected IllegalArgumentException to be thrown but wasn't");
+ } catch (IllegalArgumentException iae) {
+ assertThat(iae)
+ .hasMessageThat()
+ .isEqualTo("tolerance (" + tolerance + ") cannot be negative");
+ }
+ }
+
+ private static void isNotWithinNegativeToleranceThrowsIAE(
+ float actual, float tolerance, float expected) {
+ try {
+ assertThat(actual).isNotWithin(tolerance).of(expected);
+ fail("Expected IllegalArgumentException to be thrown but wasn't");
+ } catch (IllegalArgumentException iae) {
+ assertThat(iae)
+ .hasMessageThat()
+ .isEqualTo("tolerance (" + tolerance + ") cannot be negative");
+ }
+ }
+
+ @Test
+ public void nanTolerances() {
+ try {
+ assertThat(1.0f).isWithin(Float.NaN).of(1.0f);
+ fail("Expected IllegalArgumentException to be thrown but wasn't");
+ } catch (IllegalArgumentException iae) {
+ assertThat(iae).hasMessageThat().isEqualTo("tolerance cannot be NaN");
+ }
+ try {
+ assertThat(1.0f).isNotWithin(Float.NaN).of(2.0f);
+ fail("Expected IllegalArgumentException to be thrown but wasn't");
+ } catch (IllegalArgumentException iae) {
+ assertThat(iae).hasMessageThat().isEqualTo("tolerance cannot be NaN");
+ }
+ }
+
+ @Test
+ public void infiniteTolerances() {
+ try {
+ assertThat(1.0f).isWithin(Float.POSITIVE_INFINITY).of(1.0f);
+ fail("Expected IllegalArgumentException to be thrown but wasn't");
+ } catch (IllegalArgumentException iae) {
+ assertThat(iae).hasMessageThat().isEqualTo("tolerance cannot be POSITIVE_INFINITY");
+ }
+ try {
+ assertThat(1.0f).isNotWithin(Float.POSITIVE_INFINITY).of(2.0f);
+ fail("Expected IllegalArgumentException to be thrown but wasn't");
+ } catch (IllegalArgumentException iae) {
+ assertThat(iae).hasMessageThat().isEqualTo("tolerance cannot be POSITIVE_INFINITY");
+ }
+ }
+
+ @Test
+ public void isWithinOfZero() {
+ assertThat(+0.0f).isWithin(0.00001f).of(+0.0f);
+ assertThat(+0.0f).isWithin(0.00001f).of(-0.0f);
+ assertThat(-0.0f).isWithin(0.00001f).of(+0.0f);
+ assertThat(-0.0f).isWithin(0.00001f).of(-0.0f);
+
+ assertThat(+0.0f).isWithin(0.0f).of(+0.0f);
+ assertThat(+0.0f).isWithin(0.0f).of(-0.0f);
+ assertThat(-0.0f).isWithin(0.0f).of(+0.0f);
+ assertThat(-0.0f).isWithin(0.0f).of(-0.0f);
+ }
+
+ @Test
+ public void isNotWithinOfZero() {
+ assertThat(+0.0f).isNotWithin(0.00001f).of(+1.0f);
+ assertThat(+0.0f).isNotWithin(0.00001f).of(-1.0f);
+ assertThat(-0.0f).isNotWithin(0.00001f).of(+1.0f);
+ assertThat(-0.0f).isNotWithin(0.00001f).of(-1.0f);
+
+ assertThat(+1.0f).isNotWithin(0.00001f).of(+0.0f);
+ assertThat(+1.0f).isNotWithin(0.00001f).of(-0.0f);
+ assertThat(-1.0f).isNotWithin(0.00001f).of(+0.0f);
+ assertThat(-1.0f).isNotWithin(0.00001f).of(-0.0f);
+
+ assertThat(+1.0f).isNotWithin(0.0f).of(+0.0f);
+ assertThat(+1.0f).isNotWithin(0.0f).of(-0.0f);
+ assertThat(-1.0f).isNotWithin(0.0f).of(+0.0f);
+ assertThat(-1.0f).isNotWithin(0.0f).of(-0.0f);
+
+ assertThatIsNotWithinFails(-0.0f, 0.0f, 0.0f);
+ }
+
+ @Test
+ public void isWithinZeroTolerance() {
+ float max = Float.MAX_VALUE;
+ assertThat(max).isWithin(0.0f).of(max);
+ assertThat(NEARLY_MAX).isWithin(0.0f).of(NEARLY_MAX);
+ assertThatIsWithinFails(max, 0.0f, NEARLY_MAX);
+ assertThatIsWithinFails(NEARLY_MAX, 0.0f, max);
+
+ float negativeMax = -1.0f * Float.MAX_VALUE;
+ assertThat(negativeMax).isWithin(0.0f).of(negativeMax);
+ assertThat(NEGATIVE_NEARLY_MAX).isWithin(0.0f).of(NEGATIVE_NEARLY_MAX);
+ assertThatIsWithinFails(negativeMax, 0.0f, NEGATIVE_NEARLY_MAX);
+ assertThatIsWithinFails(NEGATIVE_NEARLY_MAX, 0.0f, negativeMax);
+
+ float min = Float.MIN_VALUE;
+ assertThat(min).isWithin(0.0f).of(min);
+ assertThat(JUST_OVER_MIN).isWithin(0.0f).of(JUST_OVER_MIN);
+ assertThatIsWithinFails(min, 0.0f, JUST_OVER_MIN);
+ assertThatIsWithinFails(JUST_OVER_MIN, 0.0f, min);
+
+ float negativeMin = -1.0f * Float.MIN_VALUE;
+ assertThat(negativeMin).isWithin(0.0f).of(negativeMin);
+ assertThat(JUST_UNDER_NEGATIVE_MIN).isWithin(0.0f).of(JUST_UNDER_NEGATIVE_MIN);
+ assertThatIsWithinFails(negativeMin, 0.0f, JUST_UNDER_NEGATIVE_MIN);
+ assertThatIsWithinFails(JUST_UNDER_NEGATIVE_MIN, 0.0f, negativeMin);
+ }
+
+ @Test
+ public void isNotWithinZeroTolerance() {
+ float max = Float.MAX_VALUE;
+ assertThatIsNotWithinFails(max, 0.0f, max);
+ assertThatIsNotWithinFails(NEARLY_MAX, 0.0f, NEARLY_MAX);
+ assertThat(max).isNotWithin(0.0f).of(NEARLY_MAX);
+ assertThat(NEARLY_MAX).isNotWithin(0.0f).of(max);
+
+ float min = Float.MIN_VALUE;
+ assertThatIsNotWithinFails(min, 0.0f, min);
+ assertThatIsNotWithinFails(JUST_OVER_MIN, 0.0f, JUST_OVER_MIN);
+ assertThat(min).isNotWithin(0.0f).of(JUST_OVER_MIN);
+ assertThat(JUST_OVER_MIN).isNotWithin(0.0f).of(min);
+ }
+
+ @Test
+ public void isWithinNonFinite() {
+ assertThatIsWithinFails(Float.NaN, 0.00001f, Float.NaN);
+ assertThatIsWithinFails(Float.NaN, 0.00001f, Float.POSITIVE_INFINITY);
+ assertThatIsWithinFails(Float.NaN, 0.00001f, Float.NEGATIVE_INFINITY);
+ assertThatIsWithinFails(Float.NaN, 0.00001f, +0.0f);
+ assertThatIsWithinFails(Float.NaN, 0.00001f, -0.0f);
+ assertThatIsWithinFails(Float.NaN, 0.00001f, +1.0f);
+ assertThatIsWithinFails(Float.NaN, 0.00001f, -0.0f);
+ assertThatIsWithinFails(Float.POSITIVE_INFINITY, 0.00001f, Float.POSITIVE_INFINITY);
+ assertThatIsWithinFails(Float.POSITIVE_INFINITY, 0.00001f, Float.NEGATIVE_INFINITY);
+ assertThatIsWithinFails(Float.POSITIVE_INFINITY, 0.00001f, +0.0f);
+ assertThatIsWithinFails(Float.POSITIVE_INFINITY, 0.00001f, -0.0f);
+ assertThatIsWithinFails(Float.POSITIVE_INFINITY, 0.00001f, +1.0f);
+ assertThatIsWithinFails(Float.POSITIVE_INFINITY, 0.00001f, -0.0f);
+ assertThatIsWithinFails(Float.NEGATIVE_INFINITY, 0.00001f, Float.NEGATIVE_INFINITY);
+ assertThatIsWithinFails(Float.NEGATIVE_INFINITY, 0.00001f, +0.0f);
+ assertThatIsWithinFails(Float.NEGATIVE_INFINITY, 0.00001f, -0.0f);
+ assertThatIsWithinFails(Float.NEGATIVE_INFINITY, 0.00001f, +1.0f);
+ assertThatIsWithinFails(Float.NEGATIVE_INFINITY, 0.00001f, -0.0f);
+ assertThatIsWithinFails(+1.0f, 0.00001f, Float.NaN);
+ assertThatIsWithinFails(+1.0f, 0.00001f, Float.POSITIVE_INFINITY);
+ assertThatIsWithinFails(+1.0f, 0.00001f, Float.NEGATIVE_INFINITY);
+ }
+
+ @Test
+ public void isNotWithinNonFinite() {
+ assertThatIsNotWithinFails(Float.NaN, 0.00001f, Float.NaN);
+ assertThatIsNotWithinFails(Float.NaN, 0.00001f, Float.POSITIVE_INFINITY);
+ assertThatIsNotWithinFails(Float.NaN, 0.00001f, Float.NEGATIVE_INFINITY);
+ assertThatIsNotWithinFails(Float.NaN, 0.00001f, +0.0f);
+ assertThatIsNotWithinFails(Float.NaN, 0.00001f, -0.0f);
+ assertThatIsNotWithinFails(Float.NaN, 0.00001f, +1.0f);
+ assertThatIsNotWithinFails(Float.NaN, 0.00001f, -0.0f);
+ assertThatIsNotWithinFails(Float.POSITIVE_INFINITY, 0.00001f, Float.POSITIVE_INFINITY);
+ assertThatIsNotWithinFails(Float.POSITIVE_INFINITY, 0.00001f, Float.NEGATIVE_INFINITY);
+ assertThatIsNotWithinFails(Float.POSITIVE_INFINITY, 0.00001f, +0.0f);
+ assertThatIsNotWithinFails(Float.POSITIVE_INFINITY, 0.00001f, -0.0f);
+ assertThatIsNotWithinFails(Float.POSITIVE_INFINITY, 0.00001f, +1.0f);
+ assertThatIsNotWithinFails(Float.POSITIVE_INFINITY, 0.00001f, -0.0f);
+ assertThatIsNotWithinFails(Float.NEGATIVE_INFINITY, 0.00001f, Float.NEGATIVE_INFINITY);
+ assertThatIsNotWithinFails(Float.NEGATIVE_INFINITY, 0.00001f, +0.0f);
+ assertThatIsNotWithinFails(Float.NEGATIVE_INFINITY, 0.00001f, -0.0f);
+ assertThatIsNotWithinFails(Float.NEGATIVE_INFINITY, 0.00001f, +1.0f);
+ assertThatIsNotWithinFails(Float.NEGATIVE_INFINITY, 0.00001f, -0.0f);
+ assertThatIsNotWithinFails(+1.0f, 0.00001f, Float.NaN);
+ assertThatIsNotWithinFails(+1.0f, 0.00001f, Float.POSITIVE_INFINITY);
+ assertThatIsNotWithinFails(+1.0f, 0.00001f, Float.NEGATIVE_INFINITY);
+ }
+
+ @SuppressWarnings("TruthSelfEquals")
+ @Test
+ public void isEqualTo() {
+ assertThat(GOLDEN).isEqualTo(GOLDEN);
+ assertThatIsEqualToFails(GOLDEN, JUST_OVER_GOLDEN);
+ assertThat(Float.POSITIVE_INFINITY).isEqualTo(Float.POSITIVE_INFINITY);
+ assertThat(Float.NaN).isEqualTo(Float.NaN);
+ assertThat((Float) null).isEqualTo(null);
+ assertThat(1.0f).isEqualTo(1);
+ }
+
+ private static void assertThatIsEqualToFails(final float actual, final float expected) {
+ ExpectFailure.SimpleSubjectBuilderCallback<FloatSubject, Float> callback =
+ new ExpectFailure.SimpleSubjectBuilderCallback<FloatSubject, Float>() {
+ @Override
+ public void invokeAssertion(SimpleSubjectBuilder<FloatSubject, Float> expect) {
+ expect.that(actual).isEqualTo(expected);
+ }
+ };
+ expectFailure(callback);
+ }
+
+ @Test
+ public void isNotEqualTo() {
+ assertThatIsNotEqualToFails(GOLDEN);
+ assertThat(GOLDEN).isNotEqualTo(JUST_OVER_GOLDEN);
+ assertThatIsNotEqualToFails(Float.POSITIVE_INFINITY);
+ assertThatIsNotEqualToFails(Float.NaN);
+ assertThat(-0.0f).isNotEqualTo(0.0f);
+ assertThatIsNotEqualToFails(null);
+ assertThat(1.23f).isNotEqualTo(1.23);
+ assertThat(1.0f).isNotEqualTo(2);
+ }
+
+ private static void assertThatIsNotEqualToFails(final @Nullable Float value) {
+ ExpectFailure.SimpleSubjectBuilderCallback<FloatSubject, Float> callback =
+ new ExpectFailure.SimpleSubjectBuilderCallback<FloatSubject, Float>() {
+ @Override
+ public void invokeAssertion(SimpleSubjectBuilder<FloatSubject, Float> expect) {
+ expect.that(value).isNotEqualTo(value);
+ }
+ };
+ expectFailure(callback);
+ }
+
+ @Test
+ public void isZero() {
+ assertThat(0.0f).isZero();
+ assertThat(-0.0f).isZero();
+ assertThatIsZeroFails(Float.MIN_VALUE);
+ assertThatIsZeroFails(-1.23f);
+ assertThatIsZeroFails(Float.POSITIVE_INFINITY);
+ assertThatIsZeroFails(Float.NaN);
+ assertThatIsZeroFails(null);
+ }
+
+ private static void assertThatIsZeroFails(final @Nullable Float value) {
+ ExpectFailure.SimpleSubjectBuilderCallback<FloatSubject, Float> callback =
+ new ExpectFailure.SimpleSubjectBuilderCallback<FloatSubject, Float>() {
+ @Override
+ public void invokeAssertion(SimpleSubjectBuilder<FloatSubject, Float> expect) {
+ expect.that(value).isZero();
+ }
+ };
+ AssertionError failure = expectFailure(callback);
+ assertThat(failure).factKeys().containsExactly("expected zero", "but was").inOrder();
+ }
+
+ @Test
+ public void isNonZero() {
+ assertThatIsNonZeroFails(0.0f, "expected not to be zero");
+ assertThatIsNonZeroFails(-0.0f, "expected not to be zero");
+ assertThat(Float.MIN_VALUE).isNonZero();
+ assertThat(-1.23f).isNonZero();
+ assertThat(Float.POSITIVE_INFINITY).isNonZero();
+ assertThat(Float.NaN).isNonZero();
+ assertThatIsNonZeroFails(null, "expected a float other than zero");
+ }
+
+ private static void assertThatIsNonZeroFails(final @Nullable Float value, String factKey) {
+ ExpectFailure.SimpleSubjectBuilderCallback<FloatSubject, Float> callback =
+ new ExpectFailure.SimpleSubjectBuilderCallback<FloatSubject, Float>() {
+ @Override
+ public void invokeAssertion(SimpleSubjectBuilder<FloatSubject, Float> expect) {
+ expect.that(value).isNonZero();
+ }
+ };
+ AssertionError failure = expectFailure(callback);
+ assertThat(failure).factKeys().containsExactly(factKey, "but was").inOrder();
+ }
+
+ @Test
+ public void isPositiveInfinity() {
+ assertThat(Float.POSITIVE_INFINITY).isPositiveInfinity();
+ assertThatIsPositiveInfinityFails(1.23f);
+ assertThatIsPositiveInfinityFails(Float.NEGATIVE_INFINITY);
+ assertThatIsPositiveInfinityFails(Float.NaN);
+ assertThatIsPositiveInfinityFails(null);
+ }
+
+ private static void assertThatIsPositiveInfinityFails(final @Nullable Float value) {
+ ExpectFailure.SimpleSubjectBuilderCallback<FloatSubject, Float> callback =
+ new ExpectFailure.SimpleSubjectBuilderCallback<FloatSubject, Float>() {
+ @Override
+ public void invokeAssertion(SimpleSubjectBuilder<FloatSubject, Float> expect) {
+ expect.that(value).isPositiveInfinity();
+ }
+ };
+ expectFailure(callback);
+ }
+
+ @Test
+ public void isNegativeInfinity() {
+ assertThat(Float.NEGATIVE_INFINITY).isNegativeInfinity();
+ assertThatIsNegativeInfinityFails(1.23f);
+ assertThatIsNegativeInfinityFails(Float.POSITIVE_INFINITY);
+ assertThatIsNegativeInfinityFails(Float.NaN);
+ assertThatIsNegativeInfinityFails(null);
+ }
+
+ private static void assertThatIsNegativeInfinityFails(final @Nullable Float value) {
+ ExpectFailure.SimpleSubjectBuilderCallback<FloatSubject, Float> callback =
+ new ExpectFailure.SimpleSubjectBuilderCallback<FloatSubject, Float>() {
+ @Override
+ public void invokeAssertion(SimpleSubjectBuilder<FloatSubject, Float> expect) {
+ expect.that(value).isNegativeInfinity();
+ }
+ };
+ expectFailure(callback);
+ }
+
+ @Test
+ public void isNaN() {
+ assertThat(Float.NaN).isNaN();
+ assertThatIsNaNFails(1.23f);
+ assertThatIsNaNFails(Float.POSITIVE_INFINITY);
+ assertThatIsNaNFails(Float.NEGATIVE_INFINITY);
+ assertThatIsNaNFails(null);
+ }
+
+ private static void assertThatIsNaNFails(final @Nullable Float value) {
+ ExpectFailure.SimpleSubjectBuilderCallback<FloatSubject, Float> callback =
+ new ExpectFailure.SimpleSubjectBuilderCallback<FloatSubject, Float>() {
+ @Override
+ public void invokeAssertion(SimpleSubjectBuilder<FloatSubject, Float> expect) {
+ expect.that(value).isNaN();
+ }
+ };
+ expectFailure(callback);
+ }
+
+ @Test
+ public void isFinite() {
+ assertThat(1.23f).isFinite();
+ assertThat(Float.MAX_VALUE).isFinite();
+ assertThat(-1.0 * Float.MIN_VALUE).isFinite();
+ assertThatIsFiniteFails(Float.POSITIVE_INFINITY);
+ assertThatIsFiniteFails(Float.NEGATIVE_INFINITY);
+ assertThatIsFiniteFails(Float.NaN);
+ assertThatIsFiniteFails(null);
+ }
+
+ private static void assertThatIsFiniteFails(final @Nullable Float value) {
+ ExpectFailure.SimpleSubjectBuilderCallback<FloatSubject, Float> callback =
+ new ExpectFailure.SimpleSubjectBuilderCallback<FloatSubject, Float>() {
+ @Override
+ public void invokeAssertion(SimpleSubjectBuilder<FloatSubject, Float> expect) {
+ expect.that(value).isFinite();
+ }
+ };
+ AssertionError failure = expectFailure(callback);
+ assertThat(failure).factKeys().containsExactly("expected to be finite", "but was").inOrder();
+ }
+
+ @Test
+ public void isNotNaN() {
+ assertThat(1.23f).isNotNaN();
+ assertThat(Float.MAX_VALUE).isNotNaN();
+ assertThat(-1.0 * Float.MIN_VALUE).isNotNaN();
+ assertThat(Float.POSITIVE_INFINITY).isNotNaN();
+ assertThat(Float.NEGATIVE_INFINITY).isNotNaN();
+ }
+
+ @Test
+ public void isNotNaNIsNaN() {
+ expectFailureWhenTestingThat(Float.NaN).isNotNaN();
+ }
+
+ @Test
+ public void isNotNaNIsNull() {
+ expectFailureWhenTestingThat(null).isNotNaN();
+ assertFailureKeys("expected a float other than NaN", "but was");
+ }
+
+ @Test
+ public void isGreaterThan_int_strictly() {
+ expectFailureWhenTestingThat(2.0f).isGreaterThan(3);
+ }
+
+ @Test
+ public void isGreaterThan_int() {
+ expectFailureWhenTestingThat(2.0f).isGreaterThan(2);
+ assertThat(2.0f).isGreaterThan(1);
+ assertThat(0x1.0p30f).isGreaterThan((1 << 30) - 1);
+ }
+
+ @Test
+ public void isLessThan_int_strictly() {
+ expectFailureWhenTestingThat(2.0f).isLessThan(1);
+ }
+
+ @Test
+ public void isLessThan_int() {
+ expectFailureWhenTestingThat(2.0f).isLessThan(2);
+ assertThat(2.0f).isLessThan(3);
+ assertThat(0x1.0p30f).isLessThan((1 << 30) + 1);
+ }
+
+ @Test
+ public void isAtLeast_int() {
+ expectFailureWhenTestingThat(2.0f).isAtLeast(3);
+ assertThat(2.0f).isAtLeast(2);
+ assertThat(2.0f).isAtLeast(1);
+ }
+
+ @Test
+ public void isAtLeast_int_withNoExactFloatRepresentation() {
+ expectFailureWhenTestingThat(0x1.0p30f).isAtLeast((1 << 30) + 1);
+ }
+
+ @Test
+ public void isAtMost_int() {
+ expectFailureWhenTestingThat(2.0f).isAtMost(1);
+ assertThat(2.0f).isAtMost(2);
+ assertThat(2.0f).isAtMost(3);
+ }
+
+ @Test
+ public void isAtMost_int_withNoExactFloatRepresentation() {
+ expectFailureWhenTestingThat(0x1.0p30f).isAtMost((1 << 30) - 1);
+ }
+
+ private FloatSubject expectFailureWhenTestingThat(Float actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/GraphMatchingTest.java b/core/src/test/java/com/google/common/truth/GraphMatchingTest.java
new file mode 100644
index 00000000..9a5f0ed5
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/GraphMatchingTest.java
@@ -0,0 +1,459 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.GraphMatching.maximumCardinalityBipartiteMatching;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.fail;
+
+import com.google.common.annotations.GwtIncompatible;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import com.google.common.collect.ImmutableBiMap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import java.util.ArrayDeque;
+import java.util.BitSet;
+import java.util.Deque;
+import java.util.Map;
+import java.util.Random;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link GraphMatching}.
+ *
+ * @author Pete Gillin
+ */
+@RunWith(JUnit4.class)
+public final class GraphMatchingTest {
+
+ @Test
+ public void maximumCardinalityBipartiteMatching_empty() {
+ TestInstance.empty().testAgainstKnownSize(0);
+ }
+
+ @Test
+ public void maximumCardinalityBipartiteMatching_exhaustive3x4() {
+ for (int edgeCombination = 1; edgeCombination < (1L << (3 * 4)); edgeCombination++) {
+ TestInstance.fromBits(3, 4, intBits(edgeCombination)).testAgainstBruteForce();
+ }
+ }
+
+ @Test
+ @GwtIncompatible("slow")
+ public void maximumCardinalityBipartiteMatching_exhaustive4x4() {
+ if (Platform.isAndroid()) {
+ return; // slow
+ }
+ for (int edgeCombination = 1; edgeCombination < (1L << (4 * 4)); edgeCombination++) {
+ TestInstance.fromBits(4, 4, intBits(edgeCombination)).testAgainstBruteForce();
+ }
+ }
+
+ @Test
+ @GwtIncompatible("slow")
+ public void maximumCardinalityBipartiteMatching_exhaustive3x5() {
+ if (Platform.isAndroid()) {
+ return; // slow
+ }
+ for (int edgeCombination = 1; edgeCombination < (1L << (3 * 5)); edgeCombination++) {
+ TestInstance.fromBits(3, 5, intBits(edgeCombination)).testAgainstBruteForce();
+ }
+ }
+
+ @Test
+ @GwtIncompatible("slow")
+ public void maximumCardinalityBipartiteMatching_exhaustive5x3() {
+ if (Platform.isAndroid()) {
+ return; // slow
+ }
+ for (int edgeCombination = 1; edgeCombination < (1L << (5 * 3)); edgeCombination++) {
+ TestInstance.fromBits(5, 3, intBits(edgeCombination)).testAgainstBruteForce();
+ }
+ }
+
+ @Test
+ public void maximumCardinalityBipartiteMatching_fullyConnected8x8() {
+ TestInstance.fullyConnected(8, 8).testAgainstKnownSize(8);
+ }
+
+ @Test
+ public void maximumCardinalityBipartiteMatching_random8x8() {
+ Random rng = new Random(0x5ca1ab1e);
+ for (int i = 0; i < 100; i++) {
+ // Set each bit with probability 0.25, giving an average of 2 of the possible 8 edges per
+ // vertex. By observation, the maximal matching most commonly has cardinality 6 (although
+ // occasionally you do see a complete matching i.e. cardinality 8).
+ TestInstance.fromBits(8, 8, randomBits(8 * 8, 0.25, rng)).testAgainstBruteForce();
+ }
+ }
+
+ @Test
+ public void maximumCardinalityBipartiteMatching_randomSparse8x8() {
+ Random rng = new Random(0x0ddba11);
+ for (int i = 0; i < 100; i++) {
+ // Set each bit with probability 0.125, giving an average of 1 of the possible 8 edges per
+ // vertex. By observation, the maximal matching most commonly has cardinality 4.
+ TestInstance.fromBits(8, 8, randomBits(8 * 8, 0.125, rng)).testAgainstBruteForce();
+ }
+ }
+
+ @Test
+ @GwtIncompatible("slow")
+ public void maximumCardinalityBipartiteMatching_randomDense8x8() {
+ if (Platform.isAndroid()) {
+ return; // slow
+ }
+ Random rng = new Random(0x5add1e5);
+ for (int i = 0; i < 100; i++) {
+ // Set each bit with probability 0.5, giving an average of 4 of the possible 8 edges per
+ // vertex. By observation, a complete matching is almost always possible (although
+ // occasionally you do see a maximum cardinality of 7 or even fewer).
+ TestInstance.fromBits(8, 8, randomBits(8 * 8, 0.5, rng)).testAgainstBruteForce();
+ }
+ }
+
+ @Test
+ public void maximumCardinalityBipartiteMatching_failsWithNullLhs() {
+ ListMultimap<String, String> edges = LinkedListMultimap.create();
+ edges.put(null, "R1");
+ try {
+ BiMap<String, String> unused = maximumCardinalityBipartiteMatching(edges);
+ fail("Should have thrown.");
+ } catch (NullPointerException expected) {
+ }
+ }
+
+ @Test
+ public void maximumCardinalityBipartiteMatching_failsWithNullRhs() {
+ ListMultimap<String, String> edges = LinkedListMultimap.create();
+ edges.put("L1", null);
+ try {
+ BiMap<String, String> unused = maximumCardinalityBipartiteMatching(edges);
+ fail("Should have thrown.");
+ } catch (NullPointerException expected) {
+ }
+ }
+
+ /** Representation of a bipartite graph to be used for testing. */
+ private static class TestInstance {
+
+ /** Generates a test instance with an empty bipartite graph. */
+ static TestInstance empty() {
+ return new TestInstance(ImmutableListMultimap.<String, String>of());
+ }
+
+ /**
+ * Generates a test instance with a fully-connected bipartite graph where there are {@code
+ * lhsSize} elements in one set of vertices (which we call the LHS) and {@code rhsSize} elements
+ * in the other (the RHS).
+ */
+ static TestInstance fullyConnected(int lhsSize, int rhsSize) {
+ ImmutableListMultimap.Builder<String, String> edges = ImmutableListMultimap.builder();
+ for (int lhs = 0; lhs < lhsSize; lhs++) {
+ for (int rhs = 0; rhs < rhsSize; rhs++) {
+ edges.put("L" + lhs, "R" + rhs);
+ }
+ }
+ return new TestInstance(edges.build());
+ }
+
+ /**
+ * Generates a test instance with a bipartite graph where there are {@code lhsSize} elements in
+ * one set of vertices (which we call the LHS) and {@code rhsSize} elements in the other (the
+ * RHS) and whether or not each of the {@code lhsSize * rhsSize} possible edges is included or
+ * not according to whether one of the first {@code lhsSize * rhsSize} bits of {@code bits} is
+ * set or not.
+ */
+ static TestInstance fromBits(int lhsSize, int rhsSize, BitSet bits) {
+ ImmutableListMultimap.Builder<String, String> edges = ImmutableListMultimap.builder();
+ for (int lhs = 0; lhs < lhsSize; lhs++) {
+ for (int rhs = 0; rhs < rhsSize; rhs++) {
+ if (bits.get(lhs * rhsSize + rhs)) {
+ edges.put("L" + lhs, "R" + rhs);
+ }
+ }
+ }
+ return new TestInstance(edges.build());
+ }
+
+ private final ImmutableListMultimap<String, String> edges;
+ private final ImmutableList<String> lhsVertices;
+
+ private TestInstance(ImmutableListMultimap<String, String> edges) {
+ this.edges = edges;
+ this.lhsVertices = edges.keySet().asList();
+ }
+
+ /**
+ * Finds the maximum bipartite matching using the method under test and asserts both that it is
+ * actually a matching of this bipartite graph and that it has the same size as a maximum
+ * bipartite matching found by a brute-force approach.
+ */
+ void testAgainstBruteForce() {
+ ImmutableBiMap<String, String> actual = maximumCardinalityBipartiteMatching(edges);
+ for (Map.Entry<String, String> entry : actual.entrySet()) {
+ assertWithMessage(
+ "The returned bimap <%s> was not a matching of the bipartite graph <%s>",
+ actual, edges)
+ .that(edges)
+ .containsEntry(entry.getKey(), entry.getValue());
+ }
+ ImmutableBiMap<String, String> expected = bruteForceMaximalMatching();
+ assertWithMessage(
+ "The returned matching for the bipartite graph <%s> was not the same size as "
+ + "the brute-force maximal matching <%s>",
+ edges, expected)
+ .that(actual)
+ .hasSize(expected.size());
+ }
+
+ /**
+ * Finds the maximum bipartite matching using the method under test and asserts both that it is
+ * actually a matching of this bipartite graph and that it has the expected size.
+ */
+ void testAgainstKnownSize(int expectedSize) {
+ ImmutableBiMap<String, String> actual = maximumCardinalityBipartiteMatching(edges);
+ for (Map.Entry<String, String> entry : actual.entrySet()) {
+ assertWithMessage(
+ "The returned bimap <%s> was not a matching of the bipartite graph <%s>",
+ actual, edges)
+ .that(edges)
+ .containsEntry(entry.getKey(), entry.getValue());
+ }
+ assertWithMessage(
+ "The returned matching for the bipartite graph <%s> had the wrong size", edges)
+ .that(actual)
+ .hasSize(expectedSize);
+ }
+
+ /**
+ * Returns a maximal bipartite matching of the bipartite graph, performing a brute force
+ * evaluation of every possible matching.
+ */
+ private ImmutableBiMap<String, String> bruteForceMaximalMatching() {
+ ImmutableBiMap<String, String> best = ImmutableBiMap.of();
+ Matching candidate = new Matching();
+ while (candidate.valid()) {
+ if (candidate.size() > best.size()) {
+ best = candidate.asBiMap();
+ }
+ candidate.advance();
+ }
+ return best;
+ }
+
+ /**
+ * Mutable representation of a non-empty matching over the graph. This is a cursor which can be
+ * advanced through the possible matchings in a fixed sequence. When advanced past the last
+ * matching in the sequence, this cursor is considered invalid.
+ */
+ private class Matching {
+
+ private final Deque<Edge> edgeStack;
+ private final BiMap<String, String> selectedEdges;
+
+ /** Constructs the first non-empty matching in the sequence. */
+ Matching() {
+ this.edgeStack = new ArrayDeque<>();
+ this.selectedEdges = HashBiMap.create();
+ if (!edges.isEmpty()) {
+ Edge firstEdge = new Edge();
+ edgeStack.addLast(firstEdge);
+ firstEdge.addToSelected();
+ }
+ }
+
+ /**
+ * Returns whether this cursor is valid. Returns true if it has been advanced past the end of
+ * the sequence.
+ */
+ boolean valid() {
+ // When advance() has advanced through all the non-empty maps, the final state is that
+ // selectedEdges is empty, so we use that state as a marker of the final invalid cursor.
+ return !selectedEdges.isEmpty();
+ }
+
+ /**
+ * Returns an immutable representation of the current state of the matching as a bimap giving
+ * the edges used in the matching, where the keys identify the vertices in the first set and
+ * the values identify the vertices in the second set. The bimap is guaranteed not to be
+ * empty. Fails if this cursor is invalid.
+ */
+ ImmutableBiMap<String, String> asBiMap() {
+ Preconditions.checkState(valid());
+ return ImmutableBiMap.copyOf(selectedEdges);
+ }
+
+ /**
+ * Returns the size (i.e. the number of edges in) the current matching, which is guaranteed to
+ * be positive (not zer). Fails if this cursor is invalid.
+ */
+ int size() {
+ Preconditions.checkState(valid());
+ return selectedEdges.size();
+ }
+
+ /**
+ * Advances to the next matching in the sequence, or invalidates the cursor if this was the
+ * last. Fails if this cursor is invalid.
+ */
+ void advance() {
+ Preconditions.checkState(valid());
+ // We essentially do a depth-first traversal through the possible matchings.
+ // First we try to add an edge.
+ Edge lastEdge = edgeStack.getLast();
+ Edge nextEdge = new Edge(lastEdge);
+ nextEdge.advance();
+ if (nextEdge.valid()) {
+ edgeStack.addLast(nextEdge);
+ nextEdge.addToSelected();
+ return;
+ }
+ // We can't add an edge, so we try to advance the edge at the top of the stack. If we can't
+ // advance that edge, we remove it and attempt to advance the new top of stack instead.
+ while (valid()) {
+ lastEdge = edgeStack.getLast();
+ lastEdge.removeFromSelected();
+ lastEdge.advance();
+ if (lastEdge.valid()) {
+ lastEdge.addToSelected();
+ return;
+ } else {
+ edgeStack.removeLast();
+ }
+ }
+ // We have reached the end of the sequence, and edgeStack is empty.
+ }
+
+ /**
+ * Mutable representation of an edge in a matching. This is a cursor which can be advanced
+ * through the possible edges in a fixed sequence. When advanced past the last edge in the
+ * sequence, this cursor is considered invalid.
+ */
+ private class Edge {
+
+ private int lhsIndex; // index into lhsVertices
+ private int rhsIndexForLhs; // index into edges.get(lhsVertices.get(lhsIndex))
+
+ /** Constructs the first edge in the sequence. */
+ Edge() {
+ this.lhsIndex = 0;
+ this.rhsIndexForLhs = 0;
+ }
+
+ /** Constructs a copy of the given edge. */
+ Edge(Edge other) {
+ this.lhsIndex = other.lhsIndex;
+ this.rhsIndexForLhs = other.rhsIndexForLhs;
+ }
+
+ /**
+ * Returns whether this cursor is valid. Returns true if it has been advanced past the end
+ * of the sequence.
+ */
+ boolean valid() {
+ // When advance() has advanced through all the edges, the final state is that lhsIndex ==
+ // lhsVertices.size(), so we use that state as a marker of the final invalid cursor.
+ return lhsIndex < lhsVertices.size();
+ }
+
+ /**
+ * Adds the current edge to the matching. Fails if either of the vertices in the edge is
+ * already in the matching. Fails if this cursor is invalid.
+ */
+ void addToSelected() {
+ Preconditions.checkState(valid());
+ Preconditions.checkState(!selectedEdges.containsKey(lhsVertex()));
+ Preconditions.checkState(!selectedEdges.containsValue(rhsVertex()));
+ selectedEdges.put(lhsVertex(), rhsVertex());
+ }
+
+ /**
+ * Removes the current edge from the matching. Fails if this edge is not in the matching.
+ * Fails if this cursor is invalid.
+ */
+ void removeFromSelected() {
+ Preconditions.checkState(valid());
+ Preconditions.checkState(selectedEdges.containsKey(lhsVertex()));
+ Preconditions.checkState(selectedEdges.get(lhsVertex()).equals(rhsVertex()));
+ selectedEdges.remove(lhsVertex());
+ }
+
+ /**
+ * Advances to the next edge in the sequence, or invalidates the cursor if this was the
+ * last. Skips over edges which cannot be added to the matching because either vertex is
+ * already in it. Fails if this cursor is invalid.
+ */
+ void advance() {
+ Preconditions.checkState(valid());
+ // We iterate over the possible edges in a lexicographical order with the LHS index as the
+ // most significant part and the RHS index as the least significant. So we first try
+ // advancing to the next RHS index for the current LHS index, and if we can't we advance
+ // to the next LHS index in the map and the first RHS index for that.
+ ++rhsIndexForLhs;
+ while (lhsIndex < lhsVertices.size()) {
+ if (!selectedEdges.containsKey(lhsVertex())) {
+ while (rhsIndexForLhs < edges.get(lhsVertex()).size()) {
+ if (!selectedEdges.containsValue(rhsVertex())) {
+ return;
+ }
+ ++rhsIndexForLhs;
+ }
+ }
+ ++lhsIndex;
+ rhsIndexForLhs = 0;
+ }
+ // We have reached the end of the sequence, and lhsIndex == lhsVertices.size().
+ }
+
+ private String lhsVertex() {
+ return lhsVertices.get(lhsIndex);
+ }
+
+ private String rhsVertex() {
+ return edges.get(lhsVertex()).get(rhsIndexForLhs);
+ }
+ }
+ }
+ }
+
+ /** Returns a bitset corresponding to the binary representation of the given integer. */
+ private static BitSet intBits(int intValue) {
+ BitSet bits = new BitSet();
+ for (int bitIndex = 0; bitIndex < Integer.SIZE; bitIndex++) {
+ bits.set(bitIndex, (intValue & (1L << bitIndex)) != 0);
+ }
+ return bits;
+ }
+
+ /**
+ * Returns a bitset of up to {@code maxBits} bits where each bit is set with a probability {@code
+ * bitProbability} using the given RNG.
+ */
+ private static BitSet randomBits(int maxBits, double bitProbability, Random rng) {
+ BitSet bits = new BitSet();
+ for (int bitIndex = 0; bitIndex < maxBits; bitIndex++) {
+ bits.set(bitIndex, rng.nextDouble() < bitProbability);
+ }
+ return bits;
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/GuavaOptionalSubjectTest.java b/core/src/test/java/com/google/common/truth/GuavaOptionalSubjectTest.java
new file mode 100644
index 00000000..77a1a6c4
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/GuavaOptionalSubjectTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Optional;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Guava {@link Optional} Subjects.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+@RunWith(JUnit4.class)
+public class GuavaOptionalSubjectTest extends BaseSubjectTestCase {
+
+ @Test
+ public void isPresent() {
+ assertThat(Optional.of("foo")).isPresent();
+ }
+
+ @Test
+ public void isPresentFailing() {
+ expectFailureWhenTestingThat(Optional.absent()).isPresent();
+ assertFailureKeys("expected to be present");
+ }
+
+ @Test
+ public void isPresentFailingNull() {
+ expectFailureWhenTestingThat(null).isPresent();
+ assertFailureKeys("expected present optional", "but was");
+ }
+
+ @Test
+ public void isAbsent() {
+ assertThat(Optional.absent()).isAbsent();
+ }
+
+ @Test
+ public void isAbsentFailing() {
+ expectFailureWhenTestingThat(Optional.of("foo")).isAbsent();
+ assertFailureKeys("expected to be absent", "but was present with value");
+ assertFailureValue("but was present with value", "foo");
+ }
+
+ @Test
+ public void isAbsentFailingNull() {
+ expectFailureWhenTestingThat(null).isAbsent();
+ assertFailureKeys("expected absent optional", "but was");
+ }
+
+ @Test
+ public void hasValue() {
+ assertThat(Optional.of("foo")).hasValue("foo");
+ }
+
+ @Test
+ public void hasValue_failingWithAbsent() {
+ expectFailureWhenTestingThat(Optional.absent()).hasValue("foo");
+ assertFailureKeys("expected to have value", "but was absent");
+ assertFailureValue("expected to have value", "foo");
+ }
+
+ @Test
+ public void hasValue_npeWithNullParameter() {
+ try {
+ assertThat(Optional.of("foo")).hasValue(null);
+ fail("Expected NPE");
+ } catch (NullPointerException expected) {
+ assertThat(expected).hasMessageThat().contains("Optional");
+ }
+ }
+
+ @Test
+ public void hasValue_failingWithWrongValue() {
+ expectFailureWhenTestingThat(Optional.of("foo")).hasValue("boo");
+ assertFailureValue("value of", "optional.get()");
+ }
+
+ private GuavaOptionalSubject expectFailureWhenTestingThat(Optional<?> actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/IntegerSubjectTest.java b/core/src/test/java/com/google/common/truth/IntegerSubjectTest.java
new file mode 100644
index 00000000..46103043
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/IntegerSubjectTest.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Integer Subjects.
+ *
+ * @author David Saff
+ * @author Christian Gruber
+ * @author Kurt Alfred Kluever
+ */
+@RunWith(JUnit4.class)
+public class IntegerSubjectTest extends BaseSubjectTestCase {
+
+ @Test
+ public void simpleEquality() {
+ assertThat(4).isEqualTo(4);
+ }
+
+ @Test
+ public void simpleInequality() {
+ assertThat(4).isNotEqualTo(5);
+ }
+
+ @Test
+ public void equalityWithLongs() {
+ assertThat(0).isEqualTo(0L);
+ expectFailureWhenTestingThat(0).isNotEqualTo(0L);
+ }
+
+ @Test
+ public void equalityFail() {
+ expectFailureWhenTestingThat(4).isEqualTo(5);
+ }
+
+ @Test
+ public void inequalityFail() {
+ expectFailureWhenTestingThat(4).isNotEqualTo(4);
+ }
+
+ @Test
+ public void equalityOfNulls() {
+ assertThat((Integer) null).isEqualTo(null);
+ }
+
+ @Test
+ public void equalityOfNullsFail_nullActual() {
+ expectFailureWhenTestingThat(null).isEqualTo(5);
+ }
+
+ @Test
+ public void equalityOfNullsFail_nullExpected() {
+ expectFailureWhenTestingThat(5).isEqualTo(null);
+ }
+
+ @Test
+ public void inequalityOfNulls() {
+ assertThat(4).isNotEqualTo(null);
+ assertThat((Integer) null).isNotEqualTo(4);
+ }
+
+ @Test
+ public void inequalityOfNullsFail() {
+ expectFailureWhenTestingThat(null).isNotEqualTo(null);
+ }
+
+ @Test
+ public void overflowOnPrimitives() {
+ assertThat(Long.MIN_VALUE).isNotEqualTo(Integer.MIN_VALUE);
+ assertThat(Long.MAX_VALUE).isNotEqualTo(Integer.MAX_VALUE);
+
+ assertThat(Integer.MIN_VALUE).isNotEqualTo(Long.MIN_VALUE);
+ assertThat(Integer.MAX_VALUE).isNotEqualTo(Long.MAX_VALUE);
+
+ assertThat(Integer.MIN_VALUE).isEqualTo((long) Integer.MIN_VALUE);
+ assertThat(Integer.MAX_VALUE).isEqualTo((long) Integer.MAX_VALUE);
+ }
+
+ @Test
+ public void overflowOnPrimitives_shouldBeEqualAfterCast_min() {
+ expectFailureWhenTestingThat(Integer.MIN_VALUE).isNotEqualTo((long) Integer.MIN_VALUE);
+ }
+
+ @Test
+ public void overflowOnPrimitives_shouldBeEqualAfterCast_max() {
+ expectFailureWhenTestingThat(Integer.MAX_VALUE).isNotEqualTo((long) Integer.MAX_VALUE);
+ }
+
+ @Test
+ public void overflowBetweenIntegerAndLong_shouldBeDifferent_min() {
+ expectFailureWhenTestingThat(Integer.MIN_VALUE).isEqualTo(Long.MIN_VALUE);
+ }
+
+ @Test
+ public void overflowBetweenIntegerAndLong_shouldBeDifferent_max() {
+ expectFailureWhenTestingThat(Integer.MAX_VALUE).isEqualTo(Long.MAX_VALUE);
+ }
+
+ @SuppressWarnings("TruthSelfEquals")
+ @Test
+ public void testPrimitivesVsBoxedPrimitivesVsObject_int() {
+ int int42 = 42;
+ Integer integer42 = new Integer(42);
+ Object object42 = (Object) 42;
+
+ assertThat(int42).isEqualTo(int42);
+ assertThat(integer42).isEqualTo(int42);
+ assertThat(object42).isEqualTo(int42);
+
+ assertThat(int42).isEqualTo(integer42);
+ assertThat(integer42).isEqualTo(integer42);
+ assertThat(object42).isEqualTo(integer42);
+
+ assertThat(int42).isEqualTo(object42);
+ assertThat(integer42).isEqualTo(object42);
+ assertThat(object42).isEqualTo(object42);
+ }
+
+ @SuppressWarnings("TruthSelfEquals")
+ @Test
+ public void testPrimitivesVsBoxedPrimitivesVsObject_long() {
+ long longPrim42 = 42;
+ Long long42 = new Long(42);
+ Object object42 = (Object) 42L;
+
+ assertThat(longPrim42).isEqualTo(longPrim42);
+ assertThat(long42).isEqualTo(longPrim42);
+ assertThat(object42).isEqualTo(longPrim42);
+
+ assertThat(longPrim42).isEqualTo(long42);
+ assertThat(long42).isEqualTo(long42);
+ assertThat(object42).isEqualTo(long42);
+
+ assertThat(longPrim42).isEqualTo(object42);
+ assertThat(long42).isEqualTo(object42);
+ assertThat(object42).isEqualTo(object42);
+ }
+
+ @Test
+ public void testAllCombinations_pass() {
+ assertThat(42).isEqualTo(42L);
+ assertThat(42).isEqualTo(new Long(42L));
+ assertThat(new Integer(42)).isEqualTo(42L);
+ assertThat(new Integer(42)).isEqualTo(new Long(42L));
+ assertThat(42L).isEqualTo(42);
+ assertThat(42L).isEqualTo(new Integer(42));
+ assertThat(new Long(42L)).isEqualTo(42);
+ assertThat(new Long(42L)).isEqualTo(new Integer(42));
+
+ assertThat(42).isEqualTo(42);
+ assertThat(42).isEqualTo(new Integer(42));
+ assertThat(new Integer(42)).isEqualTo(42);
+ assertThat(new Integer(42)).isEqualTo(new Integer(42));
+ assertThat(42L).isEqualTo(42L);
+ assertThat(42L).isEqualTo(new Long(42L));
+ assertThat(new Long(42L)).isEqualTo(42L);
+ assertThat(new Long(42L)).isEqualTo(new Long(42L));
+ }
+
+ @Test
+ public void testNumericTypeWithSameValue_shouldBeEqual_int_long() {
+ expectFailureWhenTestingThat(42).isNotEqualTo(42L);
+ }
+
+ @Test
+ public void testNumericTypeWithSameValue_shouldBeEqual_int_int() {
+ expectFailureWhenTestingThat(42).isNotEqualTo(42);
+ }
+
+ @Test
+ public void testNumericPrimitiveTypes_isNotEqual_shouldFail_intToChar() {
+ expectFailureWhenTestingThat(42).isNotEqualTo((char) 42);
+ // 42 in ASCII is '*'
+ assertFailureValue("expected not to be", "*");
+ assertFailureValue("but was; string representation of actual value", "42");
+ }
+
+ @Test
+ public void testNumericPrimitiveTypes_isNotEqual_shouldFail_charToInt() {
+ // Uses Object overload rather than Integer.
+ expectFailure.whenTesting().that((char) 42).isNotEqualTo(42);
+ // 42 in ASCII is '*'
+ assertFailureValue("expected not to be", "42");
+ assertFailureValue("but was; string representation of actual value", "*");
+ }
+
+ private static final Subject.Factory<Subject, Object> DEFAULT_SUBJECT_FACTORY =
+ new Subject.Factory<Subject, Object>() {
+ @Override
+ public Subject createSubject(FailureMetadata metadata, Object that) {
+ return new Subject(metadata, that);
+ }
+ };
+
+ private static void expectFailure(
+ ExpectFailure.SimpleSubjectBuilderCallback<Subject, Object> callback) {
+ AssertionError unused = ExpectFailure.expectFailureAbout(DEFAULT_SUBJECT_FACTORY, callback);
+ }
+
+ @Test
+ public void testNumericPrimitiveTypes() {
+ byte byte42 = (byte) 42;
+ short short42 = (short) 42;
+ char char42 = (char) 42;
+ int int42 = 42;
+ long long42 = (long) 42;
+
+ ImmutableSet<Object> fortyTwos =
+ ImmutableSet.<Object>of(byte42, short42, char42, int42, long42);
+ for (Object actual : fortyTwos) {
+ for (Object expected : fortyTwos) {
+ assertThat(actual).isEqualTo(expected);
+ }
+ }
+
+ ImmutableSet<Object> fortyTwosNoChar = ImmutableSet.<Object>of(byte42, short42, int42, long42);
+ for (final Object actual : fortyTwosNoChar) {
+ for (final Object expected : fortyTwosNoChar) {
+ ExpectFailure.SimpleSubjectBuilderCallback<Subject, Object> actualFirst =
+ new ExpectFailure.SimpleSubjectBuilderCallback<Subject, Object>() {
+ @Override
+ public void invokeAssertion(SimpleSubjectBuilder<Subject, Object> expect) {
+ expect.that(actual).isNotEqualTo(expected);
+ }
+ };
+ ExpectFailure.SimpleSubjectBuilderCallback<Subject, Object> expectedFirst =
+ new ExpectFailure.SimpleSubjectBuilderCallback<Subject, Object>() {
+ @Override
+ public void invokeAssertion(SimpleSubjectBuilder<Subject, Object> expect) {
+ expect.that(expected).isNotEqualTo(actual);
+ }
+ };
+ expectFailure(actualFirst);
+ expectFailure(expectedFirst);
+ }
+ }
+
+ byte byte41 = (byte) 41;
+ short short41 = (short) 41;
+ char char41 = (char) 41;
+ int int41 = 41;
+ long long41 = (long) 41;
+
+ ImmutableSet<Object> fortyOnes =
+ ImmutableSet.<Object>of(byte41, short41, char41, int41, long41);
+
+ for (Object first : fortyTwos) {
+ for (Object second : fortyOnes) {
+ assertThat(first).isNotEqualTo(second);
+ assertThat(second).isNotEqualTo(first);
+ }
+ }
+ }
+
+ private IntegerSubject expectFailureWhenTestingThat(Integer actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/IterableSubjectCorrespondenceTest.java b/core/src/test/java/com/google/common/truth/IterableSubjectCorrespondenceTest.java
new file mode 100644
index 00000000..bb075e2e
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/IterableSubjectCorrespondenceTest.java
@@ -0,0 +1,2045 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Functions.identity;
+import static com.google.common.collect.Collections2.permutations;
+import static com.google.common.truth.Correspondence.equality;
+import static com.google.common.truth.Correspondence.tolerance;
+import static com.google.common.truth.TestCorrespondences.CASE_INSENSITIVE_EQUALITY;
+import static com.google.common.truth.TestCorrespondences.CASE_INSENSITIVE_EQUALITY_HALF_NULL_SAFE;
+import static com.google.common.truth.TestCorrespondences.NULL_SAFE_RECORD_ID;
+import static com.google.common.truth.TestCorrespondences.PARSED_RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10;
+import static com.google.common.truth.TestCorrespondences.PARSED_RECORD_ID;
+import static com.google.common.truth.TestCorrespondences.RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10;
+import static com.google.common.truth.TestCorrespondences.RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10_NO_DIFF;
+import static com.google.common.truth.TestCorrespondences.RECORD_DIFF_FORMATTER;
+import static com.google.common.truth.TestCorrespondences.RECORD_ID;
+import static com.google.common.truth.TestCorrespondences.STRING_PARSES_TO_INTEGER_CORRESPONDENCE;
+import static com.google.common.truth.TestCorrespondences.WITHIN_10_OF;
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.Arrays.asList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.truth.TestCorrespondences.Record;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link IterableSubject} APIs that use {@link Correspondence}.
+ *
+ * <p>Note: Most of the assertions here call {@code assertThat(someIterable)} to get an {@link
+ * IterableSubject}, and then call {@code comparingElementsUsing(someCorrespondence)} on that to get
+ * an {@link IterableSubject.UsingCorrespondence}. The test method names omit the {@code
+ * comparingElementsUsing_} prefix for brevity.
+ *
+ * @author Pete Gillin
+ */
+@RunWith(JUnit4.class)
+public class IterableSubjectCorrespondenceTest extends BaseSubjectTestCase {
+
+ @Test
+ public void contains_success() {
+ ImmutableList<String> actual = ImmutableList.of("not a number", "+123", "+456", "+789");
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .contains(456);
+ }
+
+ @Test
+ public void contains_failure() {
+ ImmutableList<String> actual = ImmutableList.of("not a number", "+123", "+456", "+789");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .contains(2345);
+ assertFailureKeys("expected to contain", "testing whether", "but was");
+ assertFailureValue("expected to contain", "2345");
+ assertFailureValue("testing whether", "actual element parses to expected element");
+ assertFailureValue("but was", "[not a number, +123, +456, +789]");
+ }
+
+ @Test
+ public void contains_handlesExceptions() {
+ // CASE_INSENSITIVE_EQUALITY.compare throws on the null actual element.
+ List<String> actual = asList("abc", null, "ghi");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(CASE_INSENSITIVE_EQUALITY)
+ .contains("DEF");
+ // We fail with the more helpful failure message about the missing value, not the NPE.
+ assertFailureKeys(
+ "expected to contain",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, DEF) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void contains_handlesExceptions_alwaysFails() {
+ List<String> actual = asList("abc", null, "ghi");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(CASE_INSENSITIVE_EQUALITY)
+ .contains("GHI");
+ // The actual list does contain the required match. However, no reasonable implementation would
+ // find that mapping without hitting the null along the way, and that throws NPE, so we are
+ // contractually required to fail.
+ assertFailureKeys(
+ "one or more exceptions were thrown while comparing elements",
+ "first exception",
+ "expected to contain",
+ "testing whether",
+ "found match (but failing because of exception)",
+ "full contents");
+ assertFailureValue("found match (but failing because of exception)", "ghi");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, GHI) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void displayingDiffsPairedBy_1arg_contains() {
+ Record expected = Record.create(2, 200);
+ ImmutableList<Record> actual =
+ ImmutableList.of(
+ Record.create(1, 100),
+ Record.create(2, 211),
+ Record.create(4, 400),
+ Record.create(2, 189),
+ Record.createWithoutId(999));
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
+ .displayingDiffsPairedBy(RECORD_ID)
+ .contains(expected);
+ assertFailureKeys(
+ "expected to contain",
+ "testing whether",
+ "but did not",
+ "though it did contain elements with correct key (2)",
+ "#1",
+ "diff",
+ "#2",
+ "diff",
+ "---",
+ "full contents");
+ assertFailureValue("#1", "2/211");
+ assertFailureValueIndexed("diff", 0, "score:11");
+ assertFailureValue("#2", "2/189");
+ assertFailureValueIndexed("diff", 1, "score:-11");
+ }
+
+ @Test
+ public void displayingDiffsPairedBy_1arg_contains_noDiff() {
+ Record expected = Record.create(2, 200);
+ ImmutableList<Record> actual =
+ ImmutableList.of(
+ Record.create(1, 100),
+ Record.create(2, 211),
+ Record.create(4, 400),
+ Record.create(2, 189),
+ Record.createWithoutId(999));
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10_NO_DIFF)
+ .displayingDiffsPairedBy(RECORD_ID)
+ .contains(expected);
+ assertFailureKeys(
+ "expected to contain",
+ "testing whether",
+ "but did not",
+ "though it did contain elements with correct key (2)",
+ "---",
+ "full contents");
+ assertFailureValue("though it did contain elements with correct key (2)", "[2/211, 2/189]");
+ }
+
+ @Test
+ public void displayingDiffsPairedBy_1arg_contains_handlesActualKeyerExceptions() {
+ Record expected = Record.create(0, 999);
+ List<Record> actual = asList(Record.create(1, 100), null, Record.create(4, 400));
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
+ .displayingDiffsPairedBy(RECORD_ID)
+ .contains(expected);
+ assertFailureKeys(
+ "expected to contain",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while keying elements for pairing",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("actualKeyFunction.apply(null) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void displayingDiffsPairedBy_1arg_contains_handlesExpectedKeyerExceptions() {
+ List<Record> actual =
+ asList(Record.create(1, 100), Record.create(2, 200), Record.create(4, 400));
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
+ .displayingDiffsPairedBy(RECORD_ID)
+ .contains(null);
+ assertFailureKeys(
+ "expected to contain",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while keying elements for pairing",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("expectedKeyFunction.apply(null) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void displayingDiffsPairedBy_1arg_contains_handlesFormatDiffExceptions() {
+ Record expected = Record.create(0, 999);
+ List<Record> actual = asList(Record.create(1, 100), null, Record.create(4, 400));
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
+ .displayingDiffsPairedBy(NULL_SAFE_RECORD_ID)
+ .contains(expected);
+ assertFailureKeys(
+ "expected to contain",
+ "testing whether",
+ "but did not",
+ "though it did contain elements with correct key (1)",
+ "---",
+ "full contents",
+ "additionally, one or more exceptions were thrown while formatting diffs",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("formatDiff(null, 0/999) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void contains_null() {
+ List<String> actual = Arrays.asList("+123", null, "+789");
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .contains(null);
+ }
+
+ @Test
+ public void wrongTypeInActual() {
+ ImmutableList<?> actual = ImmutableList.of("valid", 123);
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .contains(456);
+ assertFailureKeys(
+ "expected to contain",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(123, 456) threw java.lang.ClassCastException");
+ }
+
+ @Test
+ public void doesNotContain_success() {
+ ImmutableList<String> actual = ImmutableList.of("not a number", "+123", "+456", "+789");
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .doesNotContain(2345);
+ }
+
+ @Test
+ public void doesNotContains_failure() {
+ ImmutableList<String> actual = ImmutableList.of("not a number", "+123", "+456", "+789");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .doesNotContain(456);
+ assertFailureKeys(
+ "expected not to contain", "testing whether", "but contained", "full contents");
+ assertFailureValue("expected not to contain", "456");
+ assertFailureValue("but contained", "[+456]");
+ }
+
+ @Test
+ public void doesNotContain_handlesExceptions() {
+ // CASE_INSENSITIVE_EQUALITY.compare throws on the null actual element.
+ List<String> actual = asList("abc", null, "ghi");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(CASE_INSENSITIVE_EQUALITY)
+ .doesNotContain("GHI");
+ // We fail with the more helpful failure message about the unexpected value, not the NPE.
+ assertFailureKeys(
+ "expected not to contain",
+ "testing whether",
+ "but contained",
+ "full contents",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, GHI) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void doesNotContain_handlesExceptions_alwaysFails() {
+ List<String> actual = asList("abc", null, "ghi");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(CASE_INSENSITIVE_EQUALITY)
+ .doesNotContain("DEF");
+ // The actual list does not contain the forbidden match. However, we cannot establish that
+ // without hitting the null along the way, and that throws NPE, so we are contractually required
+ // to fail.
+ assertFailureKeys(
+ "one or more exceptions were thrown while comparing elements",
+ "first exception",
+ "expected not to contain",
+ "testing whether",
+ "found no match (but failing because of exception)",
+ "full contents");
+ assertFailureValue("expected not to contain", "DEF");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, DEF) threw java.lang.NullPointerException");
+ assertFailureValue("expected not to contain", "DEF");
+ }
+
+ @Test
+ public void containsExactlyElementsIn_inOrder_success() {
+ ImmutableList<Integer> expected = ImmutableList.of(64, 128, 256, 128);
+ ImmutableList<String> actual = ImmutableList.of("+64", "+128", "+256", "0x80");
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyElementsIn(expected)
+ .inOrder();
+ }
+
+ @Test
+ public void containsExactlyElementsIn_successOutOfOrder() {
+ ImmutableList<Integer> expected = ImmutableList.of(64, 128, 256, 128);
+ ImmutableList<String> actual = ImmutableList.of("+128", "+64", "0x80", "+256");
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyElementsIn(expected);
+ }
+
+ @Test
+ public void containsExactlyElementsIn_outOfOrderDoesNotStringify() {
+ CountsToStringCalls o = new CountsToStringCalls();
+ List<Object> actual = asList(o, 1);
+ List<Object> expected = asList(1, o);
+ assertThat(actual).comparingElementsUsing(equality()).containsExactlyElementsIn(expected);
+ assertThat(o.calls).isEqualTo(0);
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(equality())
+ .containsExactlyElementsIn(expected)
+ .inOrder();
+ assertThat(o.calls).isGreaterThan(0);
+ }
+
+ @Test
+ public void containsExactlyElementsIn_successNonGreedy() {
+ // (We use doubles with approximate equality for this test, because we can't illustrate this
+ // case with the string parsing correspondence used in the other tests, because one string
+ // won't parse to more than one integer.)
+ ImmutableList<Double> expected = ImmutableList.of(1.0, 1.1, 1.2);
+ ImmutableList<Double> actual = ImmutableList.of(1.05, 1.15, 0.95);
+ // The comparingElementsUsing test with a tolerance of 0.1 should succeed by pairing 1.0 with
+ // 0.95, 1.1 with 1.05, and 1.2 with 1.15. A left-to-right greedy implementation would fail as
+ // it would pair 1.0 with 1.05 and 1.1 with 1.15, and fail to pair 1.2 with 0.95. Check that the
+ // implementation is truly non-greedy by testing all permutations.
+ for (List<Double> permutedActual : permutations(actual)) {
+ assertThat(permutedActual)
+ .comparingElementsUsing(tolerance(0.1))
+ .containsExactlyElementsIn(expected);
+ }
+ }
+
+ @Test
+ public void containsExactlyElementsIn_failsMissingOneCandidate() {
+ ImmutableList<Integer> expected = ImmutableList.of(64, 128, 256, 128);
+ ImmutableList<String> actual = ImmutableList.of("+64", "+128", "0x40", "0x80");
+ // Actual list has candidate matches for 64, 128, and the other 128, but is missing 256.
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys("missing (1)", "---", "expected", "testing whether", "but was");
+ assertFailureValue("missing (1)", "256");
+ assertFailureValue("expected", "[64, 128, 256, 128]");
+ assertFailureValue("testing whether", "actual element parses to expected element");
+ assertFailureValue("but was", "[+64, +128, 0x40, 0x80]");
+ }
+
+ @Test
+ public void containsExactlyElementsIn_inOrder_passesWhenBothEmpty() {
+ ImmutableList<Integer> expected = ImmutableList.of();
+ ImmutableList<String> actual = ImmutableList.of();
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyElementsIn(expected)
+ .inOrder();
+ }
+
+ @Test
+ public void containsExactlyElementsIn_failsExpectedIsEmpty() {
+ ImmutableList<Integer> expected = ImmutableList.of();
+ ImmutableList<String> actual = ImmutableList.of("+64", "+128", "0x40", "0x80");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys("expected to be empty", "but was");
+ }
+
+ @Test
+ public void containsExactlyElementsIn_failsMultipleMissingCandidates() {
+ ImmutableList<Integer> expected = ImmutableList.of(64, 128, 256, 128);
+ ImmutableList<String> actual = ImmutableList.of("+64", "+64", "0x40", "0x40");
+ // Actual list has candidate matches for 64 only, and is missing 128, 256, and the other 128.
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys("missing (3)", "---", "expected", "testing whether", "but was");
+ assertFailureValue("missing (3)", "128 [2 copies], 256");
+ }
+
+ @Test
+ public void containsExactlyElementsIn_failsOrderedMissingOneCandidate() {
+ ImmutableList<Integer> expected = ImmutableList.of(64, 128, 256, 512);
+ ImmutableList<String> actual = ImmutableList.of("+64", "+128", "+256");
+ // Actual list has in-order candidate matches for 64, 128, and 256, but is missing 512.
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys("missing (1)", "---", "expected", "testing whether", "but was");
+ assertFailureValue("missing (1)", "512");
+ }
+
+ @Test
+ public void containsExactlyElementsIn_failsExtraCandidates() {
+ ImmutableList<Integer> expected = ImmutableList.of(64, 128, 256, 128);
+ ImmutableList<String> actual = ImmutableList.of("+64", "+128", "+256", "cheese");
+ // Actual list has candidate matches for all the expected, but has extra cheese.
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys("unexpected (1)", "---", "expected", "testing whether", "but was");
+ assertFailureValue("unexpected (1)", "cheese");
+ }
+
+ @Test
+ public void containsExactlyElementsIn_failsOrderedExtraCandidates() {
+ ImmutableList<Integer> expected = ImmutableList.of(64, 128, 256, 128);
+ ImmutableList<String> actual = ImmutableList.of("+64", "+128", "+256", "0x80", "cheese");
+ // Actual list has in-order candidate matches for all the expected, but has extra cheese.
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys("unexpected (1)", "---", "expected", "testing whether", "but was");
+ assertFailureValue("unexpected (1)", "cheese");
+ }
+
+ @Test
+ public void containsExactlyElementsIn_failsMissingAndExtraCandidates() {
+ ImmutableList<Integer> expected = ImmutableList.of(64, 128, 256, 128);
+ ImmutableList<String> actual = ImmutableList.of("+64", "+128", "jalapenos", "cheese");
+ // Actual list has candidate matches for 64, 128, and the other 128, but is missing 256 and has
+ // extra jalapenos and cheese.
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys(
+ "missing (1)", "unexpected (2)", "---", "expected", "testing whether", "but was");
+ assertFailureValue("missing (1)", "256");
+ assertFailureValue("unexpected (2)", "[jalapenos, cheese]");
+ }
+
+ @Test
+ public void containsExactlyElementsIn_failsMissingAndExtraNull() {
+ ImmutableList<Integer> expected = ImmutableList.of(64, 128, 256, 128);
+ List<String> actual = asList("+64", "+128", "0x80", null);
+ // Actual list has candidate matches for 64, 128, and the other 128, but is missing 256 and has
+ // extra null. (N.B. This tests a previous regression from calling extra.toString().)
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys(
+ "missing (1)", "unexpected (1)", "---", "expected", "testing whether", "but was");
+ assertFailureValue("missing (1)", "256");
+ assertFailureValue("unexpected (1)", "[null]");
+ }
+
+ @Test
+ public void containsExactlyElementsIn_failsNullMissingAndExtra() {
+ List<Integer> expected = asList(64, 128, null, 128);
+ ImmutableList<String> actual = ImmutableList.of("+64", "+128", "0x80", "cheese");
+ // Actual list has candidate matches for 64, 128, and the other 128, but is missing null and has
+ // extra cheese.
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys(
+ "missing (1)", "unexpected (1)", "---", "expected", "testing whether", "but was");
+ assertFailureValue("missing (1)", "null");
+ assertFailureValue("unexpected (1)", "[cheese]");
+ }
+
+ @Test
+ public void containsExactlyElementsIn_handlesExceptions() {
+ ImmutableList<String> expected = ImmutableList.of("ABC", "DEF", "GHI", "JKL");
+ // CASE_INSENSITIVE_EQUALITY.compare throws on the null actual element.
+ List<String> actual = asList(null, "xyz", "abc", "def");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(CASE_INSENSITIVE_EQUALITY)
+ .containsExactlyElementsIn(expected);
+ // We fail with the more helpful failure message about the mis-matched values, not the NPE.
+ assertFailureKeys(
+ "missing (2)",
+ "unexpected (2)",
+ "---",
+ "expected",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertFailureValue("missing (2)", "GHI, JKL");
+ assertFailureValue("unexpected (2)", "null, xyz");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, ABC) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void containsExactlyElementsIn_handlesExceptions_alwaysFails() {
+ List<String> expected = asList("ABC", "DEF", "GHI", null);
+ List<String> actual = asList(null, "def", "ghi", "abc");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(CASE_INSENSITIVE_EQUALITY_HALF_NULL_SAFE)
+ .containsExactlyElementsIn(expected);
+ // CASE_INSENSITIVE_EQUALITY_HALF_NULL_SAFE.compare(null, null) returns true, so there is a
+ // mapping between actual and expected elements where they all correspond. However, no
+ // reasonable implementation would find that mapping without hitting the (null, "ABC") case
+ // along the way, and that throws NPE, so we are contractually required to fail.
+ assertFailureKeys(
+ "one or more exceptions were thrown while comparing elements",
+ "first exception",
+ "expected",
+ "testing whether",
+ "found all expected elements (but failing because of exception)",
+ "full contents");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, ABC) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void containsExactlyElementsIn_diffOneMissingSomeExtraCandidate() {
+ ImmutableList<Integer> expected = ImmutableList.of(30, 60, 90);
+ ImmutableList<Integer> actual = ImmutableList.of(101, 65, 35, 190);
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(WITHIN_10_OF)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys(
+ "missing (1)",
+ "unexpected (2)",
+ "#1",
+ "diff",
+ "#2",
+ "diff",
+ "---",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValue("missing (1)", "90");
+ assertFailureValue("#1", "101");
+ assertFailureValueIndexed("diff", 0, "11");
+ assertFailureValue("#2", "190");
+ assertFailureValueIndexed("diff", 1, "100");
+ }
+
+ @Test
+ public void displayingDiffsPairedBy_1arg_containsExactlyElementsIn() {
+ ImmutableList<Record> expected =
+ ImmutableList.of(
+ Record.create(1, 100),
+ Record.create(2, 200),
+ Record.create(3, 300),
+ Record.createWithoutId(900));
+ ImmutableList<Record> actual =
+ ImmutableList.of(
+ Record.create(1, 100),
+ Record.create(2, 211),
+ Record.create(4, 400),
+ Record.createWithoutId(999));
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
+ .displayingDiffsPairedBy(RECORD_ID)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys(
+ "for key",
+ "missing",
+ "unexpected (1)",
+ "#1",
+ "diff",
+ "---",
+ "elements without matching keys:",
+ "missing (2)",
+ "unexpected (2)",
+ "---",
+ "expected",
+ "testing whether",
+ "but was");
+ // the key=2 values:
+ assertFailureValue("for key", "2");
+ assertFailureValue("missing", "2/200");
+ assertFailureValue("#1", "2/211");
+ assertFailureValue("diff", "score:11");
+ // the values without matching keys:
+ assertFailureValue("missing (2)", "3/300, none/900");
+ assertFailureValue("unexpected (2)", "4/400, none/999");
+ }
+
+ @Test
+ public void displayingDiffsPairedBy_2arg_containsExactlyElementsIn() {
+ ImmutableList<Record> expected =
+ ImmutableList.of(
+ Record.create(1, 100),
+ Record.create(2, 200),
+ Record.create(3, 300),
+ Record.createWithoutId(900));
+ ImmutableList<String> actual = ImmutableList.of("1/100", "2/211", "4/400", "none/999");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(PARSED_RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
+ .displayingDiffsPairedBy(PARSED_RECORD_ID, RECORD_ID)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys(
+ "for key",
+ "missing",
+ "unexpected (1)",
+ "#1",
+ "diff",
+ "---",
+ "elements without matching keys:",
+ "missing (2)",
+ "unexpected (2)",
+ "---",
+ "expected",
+ "testing whether",
+ "but was");
+ // the key=2 values:
+ assertFailureValue("for key", "2");
+ assertFailureValue("missing", "2/200");
+ assertFailureValue("#1", "2/211");
+ assertFailureValue("diff", "score:11");
+ // the values without matching keys:
+ assertFailureValue("missing (2)", "3/300, none/900");
+ assertFailureValue("unexpected (2)", "4/400, none/999");
+ }
+
+ @Test
+ public void displayingDiffsPairedBy_containsExactlyElementsIn_onlyKeyed() {
+ ImmutableList<Record> expected =
+ ImmutableList.of(
+ Record.create(1, 100),
+ Record.create(2, 200),
+ Record.create(3, 300),
+ Record.createWithoutId(999));
+ ImmutableList<Record> actual =
+ ImmutableList.of(
+ Record.create(1, 100),
+ Record.create(2, 211),
+ Record.create(3, 303),
+ Record.createWithoutId(999));
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
+ .displayingDiffsPairedBy(RECORD_ID)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys(
+ "for key",
+ "missing",
+ "unexpected (1)",
+ "#1",
+ "diff",
+ "---",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValue("for key", "2");
+ assertFailureValue("missing", "2/200");
+ assertFailureValue("#1", "2/211");
+ assertFailureValue("diff", "score:11");
+ }
+
+ @Test
+ public void displayingDiffsPairedBy_containsExactlyElementsIn_noKeyed() {
+ ImmutableList<Record> expected =
+ ImmutableList.of(
+ Record.create(1, 100),
+ Record.create(2, 200),
+ Record.create(3, 300),
+ Record.createWithoutId(900));
+ ImmutableList<Record> actual =
+ ImmutableList.of(
+ Record.create(1, 100),
+ Record.create(2, 201),
+ Record.create(4, 400),
+ Record.createWithoutId(999));
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
+ .displayingDiffsPairedBy(RECORD_ID)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys(
+ "elements without matching keys:",
+ "missing (2)",
+ "unexpected (2)",
+ "---",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValue("missing (2)", "3/300, none/900");
+ assertFailureValue("unexpected (2)", "4/400, none/999");
+ }
+
+ @Test
+ public void displayingDiffsPairedBy_containsExactlyElementsIn_noDiffs() {
+ ImmutableList<Record> expected =
+ ImmutableList.of(
+ Record.create(1, 100),
+ Record.create(2, 200),
+ Record.create(3, 300),
+ Record.createWithoutId(999));
+ ImmutableList<Record> actual =
+ ImmutableList.of(
+ Record.create(1, 100),
+ Record.create(2, 211),
+ Record.create(3, 303),
+ Record.createWithoutId(999));
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10_NO_DIFF)
+ .displayingDiffsPairedBy(RECORD_ID)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys(
+ "for key", "missing", "unexpected (1)", "---", "expected", "testing whether", "but was");
+ assertFailureValue("missing", "2/200");
+ assertFailureValue("unexpected (1)", "[2/211]");
+ }
+
+ @Test
+ public void displayingDiffsPairedBy_containsExactlyElementsIn_passing() {
+ // The contract on displayingDiffsPairedBy requires that it should not affect whether the test
+ // passes or fails. This test asserts that a test which would pass on the basis of its
+ // correspondence still passes even if the user specifies a key function such that none of the
+ // elements match by key. (We advise against assertions where key function equality is stricter
+ // than correspondence, but we should still do the thing we promised we'd do in that case.)
+ ImmutableList<Double> expected = ImmutableList.of(1.0, 1.1, 1.2);
+ ImmutableList<Double> actual = ImmutableList.of(1.05, 1.15, 0.95);
+ assertThat(actual)
+ .comparingElementsUsing(tolerance(0.1))
+ .displayingDiffsPairedBy(identity())
+ .containsExactlyElementsIn(expected);
+ }
+
+ @Test
+ public void displayingDiffsPairedBy_containsExactlyElementsIn_notUnique() {
+ // The missing elements here are not uniquely keyed by the key function, so the key function
+ // should be ignored, but a warning about this should be appended to the failure message.
+ ImmutableList<Record> expected =
+ ImmutableList.of(
+ Record.create(1, 100),
+ Record.create(2, 200),
+ Record.create(3, 300),
+ Record.create(3, 301),
+ Record.createWithoutId(900));
+ ImmutableList<Record> actual =
+ ImmutableList.of(
+ Record.create(1, 100),
+ Record.create(2, 211),
+ Record.create(4, 400),
+ Record.createWithoutId(999));
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
+ .displayingDiffsPairedBy(RECORD_ID)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys(
+ "missing (4)",
+ "unexpected (3)",
+ "---",
+ "a key function which does not uniquely key the expected elements was provided and has"
+ + " consequently been ignored",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValue("missing (4)", "2/200, 3/300, 3/301, none/900");
+ assertFailureValue("unexpected (3)", "2/211, 4/400, none/999");
+ }
+
+ @Test
+ public void displayingDiffsPairedBy_containsExactlyElementsIn_handlesActualKeyerExceptions() {
+ ImmutableList<Record> expected =
+ ImmutableList.of(Record.create(1, 100), Record.create(2, 200), Record.create(4, 400));
+ List<Record> actual = asList(Record.create(1, 101), Record.create(2, 211), null);
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
+ .displayingDiffsPairedBy(RECORD_ID)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys(
+ "for key",
+ "missing",
+ "unexpected (1)",
+ "#1",
+ "diff",
+ "---",
+ "elements without matching keys:",
+ "missing (1)",
+ "unexpected (1)",
+ "---",
+ "expected",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while keying elements for pairing",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("actualKeyFunction.apply(null) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void displayingDiffsPairedBy_containsExactlyElementsIn_handlesExpectedKeyerExceptions() {
+ List<Record> expected = asList(Record.create(1, 100), Record.create(2, 200), null);
+ List<Record> actual =
+ asList(Record.create(1, 101), Record.create(2, 211), Record.create(4, 400));
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
+ .displayingDiffsPairedBy(RECORD_ID)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys(
+ "for key",
+ "missing",
+ "unexpected (1)",
+ "#1",
+ "diff",
+ "---",
+ "elements without matching keys:",
+ "missing (1)",
+ "unexpected (1)",
+ "---",
+ "expected",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while keying elements for pairing",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("expectedKeyFunction.apply(null) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void displayingDiffsPairedBy_containsExactlyElementsIn_handlesFormatDiffExceptions() {
+ ImmutableList<Record> expected =
+ ImmutableList.of(Record.create(1, 100), Record.create(2, 200), Record.create(0, 999));
+ List<Record> actual = asList(Record.create(1, 101), Record.create(2, 211), null);
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
+ .displayingDiffsPairedBy(NULL_SAFE_RECORD_ID)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys(
+ "for key",
+ "missing",
+ "unexpected (1)",
+ "#1",
+ "diff",
+ "---",
+ "for key",
+ "missing",
+ "unexpected (1)",
+ "---",
+ "expected",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while formatting diffs",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("formatDiff(null, 0/999) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void containsExactlyElementsIn_failsMissingElementInOneToOne() {
+ ImmutableList<Integer> expected = ImmutableList.of(64, 128, 256, 128);
+ ImmutableList<String> actual = ImmutableList.of("+128", "+64", "+256");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys(
+ "in an assertion requiring a 1:1 mapping between the expected and the actual elements,"
+ + " each actual element matches as least one expected element, and vice versa, but"
+ + " there was no 1:1 mapping",
+ "using the most complete 1:1 mapping (or one such mapping, if there is a tie)",
+ "missing (1)",
+ "---",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValue("missing (1)", "128");
+ }
+
+ @Test
+ public void containsExactlyElementsIn_failsExtraElementInOneToOne() {
+ ImmutableList<Integer> expected = ImmutableList.of(64, 128, 256, 128);
+ ImmutableList<String> actual = ImmutableList.of("+128", "+64", "+256", "0x80", "0x40");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys(
+ "in an assertion requiring a 1:1 mapping between the expected and the actual elements,"
+ + " each actual element matches as least one expected element, and vice versa, but"
+ + " there was no 1:1 mapping",
+ "using the most complete 1:1 mapping (or one such mapping, if there is a tie)",
+ "unexpected (1)",
+ "---",
+ "expected",
+ "testing whether",
+ "but was");
+ assertThatFailure().factValue("unexpected (1)").isAnyOf("+64", "0x40");
+ }
+
+ @Test
+ public void containsExactlyElementsIn_failsMissingAndExtraInOneToOne() {
+ ImmutableList<Integer> expected = ImmutableList.of(64, 128, 256, 128);
+ ImmutableList<String> actual = ImmutableList.of("+128", "+64", "+256", "0x40");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys(
+ "in an assertion requiring a 1:1 mapping between the expected and the actual elements,"
+ + " each actual element matches as least one expected element, and vice versa, but"
+ + " there was no 1:1 mapping",
+ "using the most complete 1:1 mapping (or one such mapping, if there is a tie)",
+ "missing (1)",
+ "unexpected (1)",
+ "---",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValue("missing (1)", "128");
+ assertThatFailure().factValue("unexpected (1)").isAnyOf("[+64]", "[0x40]");
+ }
+
+ @Test
+ public void containsExactlyElementsIn_diffOneMissingAndExtraInOneToOne() {
+ ImmutableList<Integer> expected = ImmutableList.of(30, 30, 60);
+ ImmutableList<Integer> actual = ImmutableList.of(25, 55, 65);
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(WITHIN_10_OF)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys(
+ "in an assertion requiring a 1:1 mapping between the expected and the actual elements,"
+ + " each actual element matches as least one expected element, and vice versa, but"
+ + " there was no 1:1 mapping",
+ "using the most complete 1:1 mapping (or one such mapping, if there is a tie)",
+ "missing (1)",
+ "unexpected (1)",
+ "#1",
+ "diff",
+ "---",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValue("missing (1)", "30");
+ assertThatFailure().factValue("#1").isAnyOf("55", "65");
+ assertThatFailure().factValue("diff").isAnyOf("25", "35");
+ }
+
+ @Test
+ public void containsExactlyElementsIn_inOrder_failsOutOfOrder() {
+ ImmutableList<Integer> expected = ImmutableList.of(64, 128, 256, 128);
+ ImmutableList<String> actual = ImmutableList.of("+128", "+64", "0x80", "+256");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyElementsIn(expected)
+ .inOrder();
+ assertFailureKeys(
+ "contents match, but order was wrong", "expected", "testing whether", "but was");
+ assertFailureValue("expected", "[64, 128, 256, 128]");
+ }
+
+ @Test
+ public void containsExactlyElementsIn_null() {
+ List<Integer> expected = Arrays.asList(128, null);
+ List<String> actual = Arrays.asList(null, "0x80");
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyElementsIn(expected);
+ }
+
+ @Test
+ public void containsExactlyElementsIn_array() {
+ Integer[] expected = new Integer[] {64, 128, 256, 128};
+ ImmutableList<String> actual = ImmutableList.of("+128", "+64", "0x80", "+256");
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyElementsIn(expected);
+
+ actual = ImmutableList.of("+64", "+128", "0x40", "0x80");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyElementsIn(expected);
+ assertFailureKeys("missing (1)", "---", "expected", "testing whether", "but was");
+ assertFailureValue("missing (1)", "256");
+ }
+
+ @Test
+ public void containsExactly_inOrder_success() {
+ ImmutableList<String> actual = ImmutableList.of("+64", "+128", "+256", "0x80");
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly(64, 128, 256, 128)
+ .inOrder();
+ }
+
+ @Test
+ public void containsExactly_successOutOfOrder() {
+ ImmutableList<String> actual = ImmutableList.of("+128", "+64", "0x80", "+256");
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly(64, 128, 256, 128);
+ }
+
+ @Test
+ public void containsExactly_failsMissingAndExtraInOneToOne() {
+ ImmutableList<String> actual = ImmutableList.of("+128", "+64", "+256", "0x40");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly(64, 128, 256, 128);
+ assertFailureKeys(
+ "in an assertion requiring a 1:1 mapping between the expected and the actual elements,"
+ + " each actual element matches as least one expected element, and vice versa, but"
+ + " there was no 1:1 mapping",
+ "using the most complete 1:1 mapping (or one such mapping, if there is a tie)",
+ "missing (1)",
+ "unexpected (1)",
+ "---",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValue("missing (1)", "128");
+ assertThatFailure().factValue("unexpected (1)").isAnyOf("[+64]", "[0x40]");
+ }
+
+ @Test
+ public void containsExactly_nullValueInArray() {
+ List<String> actual = Arrays.asList(null, "0x80");
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly(128, null);
+ }
+
+ @Test
+ public void containsExactly_nullArray() {
+ // Truth is tolerant of this erroneous varargs call.
+ List<String> actual = Arrays.asList((String) null);
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly((Integer[]) null)
+ .inOrder();
+ }
+
+ @Test
+ public void containsAtLeastElementsIn() {
+ ImmutableList<Integer> expected = ImmutableList.of(64, 128, 256, 128);
+ ImmutableList<String> actual =
+ ImmutableList.of("fee", "+64", "+128", "fi", "fo", "+256", "0x80", "fum");
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastElementsIn(expected)
+ .inOrder();
+ }
+
+ @Test
+ public void containsAtLeastElementsIn_inOrder_success() {
+ ImmutableList<Integer> expected = ImmutableList.of(64, 128, 256, 128);
+ ImmutableList<String> actual =
+ ImmutableList.of("fee", "+64", "+128", "fi", "fo", "+256", "0x80", "fum");
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastElementsIn(expected)
+ .inOrder();
+ }
+
+ @Test
+ public void containsAtLeastElementsIn_successOutOfOrder() {
+ ImmutableList<Integer> expected = ImmutableList.of(64, 128, 256, 128);
+ ImmutableList<String> actual =
+ ImmutableList.of("fee", "+128", "+64", "fi", "fo", "0x80", "+256", "fum");
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastElementsIn(expected);
+ }
+
+ @Test
+ public void containsAtLeastElementsIn_outOfOrderDoesNotStringify() {
+ CountsToStringCalls o = new CountsToStringCalls();
+ List<Object> actual = asList(o, 1);
+ List<Object> expected = asList(1, o);
+ assertThat(actual).comparingElementsUsing(equality()).containsAtLeastElementsIn(expected);
+ assertThat(o.calls).isEqualTo(0);
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(equality())
+ .containsAtLeastElementsIn(expected)
+ .inOrder();
+ assertThat(o.calls).isGreaterThan(0);
+ }
+
+ @Test
+ public void containsAtLeastElementsIn_successNonGreedy() {
+ // (We use doubles with approximate equality for this test, because we can't illustrate this
+ // case with the string parsing correspondence used in the other tests, because one string
+ // won't parse to more than one integer.)
+ ImmutableList<Double> expected = ImmutableList.of(1.0, 1.1, 1.2);
+ ImmutableList<Double> actual = ImmutableList.of(99.999, 1.05, 99.999, 1.15, 0.95, 99.999);
+ // The comparingElementsUsing test with a tolerance of 0.1 should succeed by pairing 1.0 with
+ // 0.95, 1.1 with 1.05, and 1.2 with 1.15. A left-to-right greedy implementation would fail as
+ // it would pair 1.0 with 1.05 and 1.1 with 1.15, and fail to pair 1.2 with 0.95. Check that the
+ // implementation is truly non-greedy by testing all permutations.
+ for (List<Double> permutedActual : permutations(actual)) {
+ assertThat(permutedActual)
+ .comparingElementsUsing(tolerance(0.1))
+ .containsAtLeastElementsIn(expected);
+ }
+ }
+
+ @Test
+ public void containsAtLeastElementsIn_failsMissingOneCandidate() {
+ ImmutableList<Integer> expected = ImmutableList.of(64, 128, 256, 128);
+ ImmutableList<String> actual =
+ ImmutableList.of("fee", "+64", "+128", "fi", "fo", "0x40", "0x80", "fum");
+ // Actual list has candidate matches for 64, 128, and the other 128, but is missing 256.
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastElementsIn(expected);
+ assertFailureKeys(
+ "missing (1)", "---", "expected to contain at least", "testing whether", "but was");
+ assertFailureValue("missing (1)", "256");
+ assertFailureValue("expected to contain at least", "[64, 128, 256, 128]");
+ assertFailureValue("testing whether", "actual element parses to expected element");
+ assertFailureValue("but was", "[fee, +64, +128, fi, fo, 0x40, 0x80, fum]");
+ }
+
+ @Test
+ public void containsAtLeastElementsIn_handlesExceptions() {
+ ImmutableList<String> expected = ImmutableList.of("ABC", "DEF", "GHI");
+ // CASE_INSENSITIVE_EQUALITY.compare throws on the null actual element.
+ List<String> actual = asList(null, "xyz", "abc", "ghi");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(CASE_INSENSITIVE_EQUALITY)
+ .containsAtLeastElementsIn(expected);
+ // We fail with the more helpful failure message about the mis-matched values, not the NPE.
+ assertFailureKeys(
+ "missing (1)",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, ABC) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void containsAtLeastElementsIn_handlesExceptions_alwaysFails() {
+ List<String> expected = asList("ABC", "DEF", null);
+ List<String> actual = asList(null, "def", "ghi", "abc");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(CASE_INSENSITIVE_EQUALITY_HALF_NULL_SAFE)
+ .containsAtLeastElementsIn(expected);
+ // CASE_INSENSITIVE_EQUALITY_HALF_NULL_SAFE.compare(null, null) returns true, so there is a
+ // mapping between actual and expected elements which includes all the expected. However, no
+ // reasonable implementation would find that mapping without hitting the (null, "ABC") case
+ // along the way, and that throws NPE, so we are contractually required to fail.
+ assertFailureKeys(
+ "one or more exceptions were thrown while comparing elements",
+ "first exception",
+ "expected to contain at least",
+ "testing whether",
+ "found all expected elements (but failing because of exception)",
+ "full contents");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, ABC) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void displayingElementsPairedBy_containsAtLeastElementsIn() {
+ ImmutableList<Record> expected =
+ ImmutableList.of(Record.create(1, 100), Record.create(2, 200), Record.createWithoutId(999));
+ ImmutableList<Record> actual =
+ ImmutableList.of(
+ Record.create(1, 101),
+ Record.create(2, 211),
+ Record.create(2, 222),
+ Record.create(3, 303),
+ Record.createWithoutId(888));
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
+ .displayingDiffsPairedBy(RECORD_ID)
+ .containsAtLeastElementsIn(expected);
+ assertFailureKeys(
+ "for key",
+ "missing",
+ "did contain elements with that key (2)",
+ "#1",
+ "diff",
+ "#2",
+ "diff",
+ "---",
+ "elements without matching keys:",
+ "missing (1)",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was");
+ // values at key 2:
+ assertFailureValue("for key", "2");
+ assertFailureValue("missing", "2/200");
+ assertFailureValue("#1", "2/211");
+ assertFailureValueIndexed("diff", 0, "score:11");
+ assertFailureValue("#2", "2/222");
+ assertFailureValueIndexed("diff", 1, "score:22");
+ // values without matching keys:
+ assertFailureValue("missing (1)", "none/999");
+ }
+
+ @Test
+ public void displayingElementsPairedBy_containsAtLeastElementsIn_notUnique() {
+ ImmutableList<Record> expected =
+ ImmutableList.of(
+ Record.create(1, 100),
+ Record.create(2, 200),
+ Record.create(2, 201),
+ Record.createWithoutId(999));
+ ImmutableList<Record> actual =
+ ImmutableList.of(Record.create(1, 101), Record.create(3, 303), Record.createWithoutId(999));
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
+ .displayingDiffsPairedBy(RECORD_ID)
+ .containsAtLeastElementsIn(expected);
+ assertFailureKeys(
+ "missing (2)",
+ "---",
+ "a key function which does not uniquely key the expected elements was provided and has"
+ + " consequently been ignored",
+ "expected to contain at least",
+ "testing whether",
+ "but was");
+ assertFailureValue("missing (2)", "2/200, 2/201");
+ }
+
+ @Test
+ public void displayingElementsPairedBy_containsAtLeastElementsIn_handlesFormatDiffExceptions() {
+ ImmutableList<Record> expected =
+ ImmutableList.of(Record.create(1, 100), Record.create(2, 200), Record.create(0, 999));
+ List<Record> actual =
+ asList(Record.create(1, 101), Record.create(2, 211), Record.create(3, 303), null);
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
+ .displayingDiffsPairedBy(NULL_SAFE_RECORD_ID)
+ .containsAtLeastElementsIn(expected);
+ assertFailureKeys(
+ "for key",
+ "missing",
+ "did contain elements with that key (1)",
+ "#1",
+ "diff",
+ "---",
+ "for key",
+ "missing",
+ "did contain elements with that key (1)",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while formatting diffs",
+ "first exception");
+ assertFailureValueIndexed("for key", 0, "2");
+ assertFailureValueIndexed("missing", 0, "2/200");
+ assertFailureValue("#1", "2/211");
+ assertFailureValue("diff", "score:11");
+ assertFailureValueIndexed("for key", 1, "0");
+ assertFailureValueIndexed("missing", 1, "0/999");
+ assertFailureValueIndexed("did contain elements with that key (1)", 1, "[null]");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("formatDiff(null, 0/999) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void containsAtLeastElementsIn_failsMultipleMissingCandidates() {
+ ImmutableList<Integer> expected = ImmutableList.of(64, 128, 256, 128);
+ ImmutableList<String> actual =
+ ImmutableList.of("fee", "+64", "+64", "fi", "fo", "0x40", "0x40", "fum");
+ // Actual list has candidate matches for 64 only, and is missing 128, 256, and the other 128.
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastElementsIn(expected);
+ assertFailureKeys(
+ "missing (3)", "---", "expected to contain at least", "testing whether", "but was");
+ assertFailureValue("missing (3)", "128 [2 copies], 256");
+ }
+
+ @Test
+ public void containsAtLeastElementsIn_failsOrderedMissingOneCandidate() {
+ ImmutableList<Integer> expected = ImmutableList.of(64, 128, 256, 512);
+ ImmutableList<String> actual =
+ ImmutableList.of("fee", "+64", "fi", "fo", "+128", "+256", "fum");
+ // Actual list has in-order candidate matches for 64, 128, and 256, but is missing 512.
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastElementsIn(expected);
+ assertFailureKeys(
+ "missing (1)", "---", "expected to contain at least", "testing whether", "but was");
+ assertFailureValue("missing (1)", "512");
+ }
+
+ @Test
+ public void containsAtLeastElementsIn_failsMissingElementInOneToOne() {
+ ImmutableList<Integer> expected = ImmutableList.of(64, 128, 256, 128);
+ ImmutableList<String> actual =
+ ImmutableList.of("fee", "+128", "fi", "fo", "+64", "+256", "fum");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastElementsIn(expected);
+ assertFailureKeys(
+ "in an assertion requiring a 1:1 mapping between the expected and a subset of the actual"
+ + " elements, each actual element matches as least one expected element, and vice"
+ + " versa, but there was no 1:1 mapping",
+ "using the most complete 1:1 mapping (or one such mapping, if there is a tie)",
+ "missing (1)",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was");
+ assertFailureValue("missing (1)", "128");
+ }
+
+ @Test
+ public void containsAtLeastElementsIn_inOrder_failsOutOfOrder() {
+ ImmutableList<Integer> expected = ImmutableList.of(64, 128, 256, 128);
+ ImmutableList<String> actual =
+ ImmutableList.of("fee", "+128", "+64", "fi", "fo", "0x80", "+256", "fum");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastElementsIn(expected)
+ .inOrder();
+ assertFailureKeys(
+ "required elements were all found, but order was wrong",
+ "expected order for required elements",
+ "testing whether",
+ "but was");
+ assertFailureValue("expected order for required elements", "[64, 128, 256, 128]");
+ }
+
+ @Test
+ public void containsAtLeastElementsIn_null() {
+ List<Integer> expected = Arrays.asList(128, null);
+ List<String> actual = Arrays.asList(null, "fee", "0x80");
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastElementsIn(expected);
+ }
+
+ @Test
+ public void containsAtLeastElementsIn_array() {
+ Integer[] expected = new Integer[] {64, 128, 256, 128};
+ ImmutableList<String> actual =
+ ImmutableList.of("fee", "+128", "+64", "fi", "fo", "0x80", "+256", "fum");
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastElementsIn(expected);
+
+ actual = ImmutableList.of("fee", "+64", "+128", "fi", "fo", "0x40", "0x80", "fum");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastElementsIn(expected);
+ assertFailureKeys(
+ "missing (1)", "---", "expected to contain at least", "testing whether", "but was");
+ assertFailureValue("missing (1)", "256");
+ }
+
+ @Test
+ public void containsAtLeast() {
+ ImmutableList<String> actual =
+ ImmutableList.of("fee", "+64", "+128", "fi", "fo", "+256", "0x80", "fum");
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeast(64, 128, 256, 128)
+ .inOrder();
+ }
+
+ @Test
+ public void containsAtLeast_inOrder_success() {
+ ImmutableList<String> actual =
+ ImmutableList.of("fee", "+64", "+128", "fi", "fo", "+256", "0x80", "fum");
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeast(64, 128, 256, 128)
+ .inOrder();
+ }
+
+ @Test
+ public void containsAtLeast_successOutOfOrder() {
+ ImmutableList<String> actual =
+ ImmutableList.of("fee", "+128", "+64", "fi", "fo", "0x80", "+256", "fum");
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeast(64, 128, 256, 128);
+ }
+
+ @Test
+ public void containsAtLeast_failsMissingElementInOneToOne() {
+ ImmutableList<String> actual =
+ ImmutableList.of("fee", "+128", "fi", "fo", "+64", "+256", "fum");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeast(64, 128, 256, 128);
+ assertFailureKeys(
+ "in an assertion requiring a 1:1 mapping between the expected and a subset of the actual"
+ + " elements, each actual element matches as least one expected element, and vice"
+ + " versa, but there was no 1:1 mapping",
+ "using the most complete 1:1 mapping (or one such mapping, if there is a tie)",
+ "missing (1)",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was");
+ assertFailureValue("missing (1)", "128");
+ }
+
+ @Test
+ public void containsAtLeast_nullValueInArray() {
+ List<String> actual = Arrays.asList(null, "fee", "0x80");
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeast(128, null);
+ }
+
+ @Test
+ public void containsAnyOf_success() {
+ ImmutableList<String> actual = ImmutableList.of("+128", "+64", "+256", "0x40");
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAnyOf(255, 256, 257);
+ }
+
+ @Test
+ public void containsAnyOf_failure() {
+ ImmutableList<String> actual =
+ ImmutableList.of("+128", "+64", "This is not the string you're looking for", "0x40");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAnyOf(255, 256, 257);
+ assertFailureKeys("expected to contain any of", "testing whether", "but was");
+ assertFailureValue("expected to contain any of", "[255, 256, 257]");
+ assertFailureValue("testing whether", "actual element parses to expected element");
+ assertFailureValue("but was", "[+128, +64, This is not the string you're looking for, 0x40]");
+ }
+
+ @Test
+ public void containsAnyOf_null() {
+ List<String> actual = asList("+128", "+64", null, "0x40");
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAnyOf(255, null, 257);
+ }
+
+ @Test
+ public void containsAnyOf_handlesExceptions() {
+ // CASE_INSENSITIVE_EQUALITY.compare throws on the null actual element.
+ List<String> actual = asList("abc", null, "ghi");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(CASE_INSENSITIVE_EQUALITY)
+ .containsAnyOf("DEF", "FED");
+ // We fail with the more helpful failure message about missing the expected values, not the NPE.
+ assertFailureKeys(
+ "expected to contain any of",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, DEF) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void containsAnyOf_handlesExceptions_alwaysFails() {
+ List<String> actual = asList("abc", null, "ghi");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(CASE_INSENSITIVE_EQUALITY)
+ .containsAnyOf("GHI", "XYZ");
+ // The actual list does contain the required match. However, no reasonable implementation would
+ // find that mapping without hitting the null along the way, and that throws NPE, so we are
+ // contractually required to fail.
+ assertFailureKeys(
+ "one or more exceptions were thrown while comparing elements",
+ "first exception",
+ "expected to contain any of",
+ "testing whether",
+ "found match (but failing because of exception)",
+ "full contents");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, GHI) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void containsAnyIn_success() {
+ ImmutableList<String> actual = ImmutableList.of("+128", "+64", "+256", "0x40");
+ ImmutableList<Integer> expected = ImmutableList.of(255, 256, 257);
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAnyIn(expected);
+ }
+
+ @Test
+ public void containsAnyIn_failure() {
+ ImmutableList<String> actual =
+ ImmutableList.of("+128", "+64", "This is not the string you're looking for", "0x40");
+ ImmutableList<Integer> expected = ImmutableList.of(255, 256, 257);
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAnyIn(expected);
+ assertFailureKeys("expected to contain any of", "testing whether", "but was");
+ assertFailureValue("expected to contain any of", "[255, 256, 257]");
+ assertFailureValue("testing whether", "actual element parses to expected element");
+ assertFailureValue("but was", "[+128, +64, This is not the string you're looking for, 0x40]");
+ }
+
+ @Test
+ public void displayingDiffsPairedBy_containsAnyIn_withKeyMatches() {
+ ImmutableList<Record> expected =
+ ImmutableList.of(
+ Record.create(1, 100),
+ Record.create(2, 200),
+ Record.create(3, 300),
+ Record.createWithoutId(999));
+ ImmutableList<Record> actual =
+ ImmutableList.of(
+ Record.create(3, 311),
+ Record.create(2, 211),
+ Record.create(2, 222),
+ Record.create(4, 404),
+ Record.createWithoutId(888));
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
+ .displayingDiffsPairedBy(RECORD_ID)
+ .containsAnyIn(expected);
+ assertFailureKeys(
+ "expected to contain any of",
+ "testing whether",
+ "but was",
+ "for key",
+ "expected any of",
+ "but got (2)",
+ "#1",
+ "diff",
+ "#2",
+ "diff",
+ "---",
+ "for key",
+ "expected any of",
+ "but got (1)",
+ "#1",
+ "diff",
+ "---");
+ // at key 2:
+ assertFailureValueIndexed("for key", 0, "2");
+ assertFailureValueIndexed("expected any of", 0, "2/200");
+ assertFailureValueIndexed("#1", 0, "2/211");
+ assertFailureValueIndexed("diff", 0, "score:11");
+ assertFailureValue("#2", "2/222");
+ assertFailureValueIndexed("diff", 1, "score:22");
+ // at key 3:
+ assertFailureValueIndexed("for key", 1, "3");
+ assertFailureValueIndexed("expected any of", 1, "3/300");
+ assertFailureValueIndexed("#1", 1, "3/311");
+ assertFailureValueIndexed("diff", 2, "score:11");
+ }
+
+ @Test
+ public void displayingDiffsPairedBy_containsAnyIn_withoutKeyMatches() {
+ ImmutableList<Record> expected =
+ ImmutableList.of(Record.create(1, 100), Record.create(2, 200), Record.createWithoutId(999));
+ ImmutableList<Record> actual =
+ ImmutableList.of(Record.create(3, 300), Record.create(4, 411), Record.createWithoutId(888));
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
+ .displayingDiffsPairedBy(RECORD_ID)
+ .containsAnyIn(expected);
+ assertFailureKeys(
+ "expected to contain any of",
+ "testing whether",
+ "but was",
+ "it does not contain any matches by key, either");
+ }
+
+ @Test
+ public void displayingDiffsPairedBy_containsAnyIn_notUnique() {
+ ImmutableList<Record> expected =
+ ImmutableList.of(
+ Record.create(1, 100),
+ Record.create(2, 200),
+ Record.create(2, 250),
+ Record.createWithoutId(999));
+ ImmutableList<Record> actual =
+ ImmutableList.of(Record.create(3, 300), Record.create(2, 211), Record.createWithoutId(888));
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
+ .displayingDiffsPairedBy(RECORD_ID)
+ .containsAnyIn(expected);
+ assertFailureKeys(
+ "expected to contain any of",
+ "testing whether",
+ "but was",
+ "a key function which does not uniquely key the expected elements was provided and has"
+ + " consequently been ignored");
+ }
+
+ @Test
+ public void displayingDiffsPairedBy_containsAnyIn_handlesFormatDiffExceptions() {
+ ImmutableList<Record> expected =
+ ImmutableList.of(Record.create(1, 100), Record.create(2, 200), Record.create(0, 999));
+ List<Record> actual = asList(Record.create(3, 311), Record.create(4, 404), null);
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
+ .displayingDiffsPairedBy(NULL_SAFE_RECORD_ID)
+ .containsAnyIn(expected);
+ assertFailureKeys(
+ "expected to contain any of",
+ "testing whether",
+ "but was",
+ "for key",
+ "expected any of",
+ "but got (1)",
+ "---",
+ "additionally, one or more exceptions were thrown while formatting diffs",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("formatDiff(null, 0/999) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void containsAnyIn_null() {
+ List<String> actual = asList("+128", "+64", null, "0x40");
+ List<Integer> expected = asList(255, null, 257);
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAnyIn(expected);
+ }
+
+ @Test
+ public void containsAnyIn_array() {
+ ImmutableList<String> actual = ImmutableList.of("+128", "+64", "+256", "0x40");
+ Integer[] expected = new Integer[] {255, 256, 257};
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAnyIn(expected);
+
+ expected = new Integer[] {511, 512, 513};
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAnyIn(expected);
+ assertFailureKeys("expected to contain any of", "testing whether", "but was");
+ }
+
+ @Test
+ public void containsNoneOf_success() {
+ ImmutableList<String> actual =
+ ImmutableList.of("+128", "+64", "This is not the string you're looking for", "0x40");
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsNoneOf(255, 256, 257);
+ }
+
+ @Test
+ public void containsNoneOf_failure() {
+ ImmutableList<String> actual = ImmutableList.of("+128", "+64", "+256", "0x40");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsNoneOf(255, 256, 257);
+ assertFailureKeys(
+ "expected not to contain any of",
+ "testing whether",
+ "but contained",
+ "corresponding to",
+ "---",
+ "full contents");
+ assertFailureValue("expected not to contain any of", "[255, 256, 257]");
+ assertFailureValue("testing whether", "actual element parses to expected element");
+ assertFailureValue("but contained", "[+256]");
+ assertFailureValue("corresponding to", "256");
+ assertFailureValue("full contents", "[+128, +64, +256, 0x40]");
+ }
+
+ @Test
+ public void containsNoneOf_multipleFailures() {
+ ImmutableList<String> actual = ImmutableList.of("+128", "+64", "+256", "0x40");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsNoneOf(64, 128);
+ assertFailureKeys(
+ "expected not to contain any of",
+ "testing whether",
+ "but contained",
+ "corresponding to",
+ "---",
+ "but contained",
+ "corresponding to",
+ "---",
+ "full contents");
+ assertFailureValueIndexed("but contained", 0, "[+64, 0x40]");
+ assertFailureValueIndexed("corresponding to", 0, "64");
+ assertFailureValueIndexed("but contained", 1, "[+128]");
+ assertFailureValueIndexed("corresponding to", 1, "128");
+ }
+
+ @Test
+ public void containsNoneOf_null() {
+ List<String> actual = asList("+128", "+64", null, "0x40");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsNoneOf(255, null, 257);
+ assertFailureKeys(
+ "expected not to contain any of",
+ "testing whether",
+ "but contained",
+ "corresponding to",
+ "---",
+ "full contents");
+ assertFailureValue("but contained", "[null]");
+ assertFailureValue("corresponding to", "null");
+ }
+
+ @Test
+ public void containsNoneOf_handlesExceptions() {
+ // CASE_INSENSITIVE_EQUALITY.compare throws on the null actual element.
+ List<String> actual = asList("abc", null, "ghi");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(CASE_INSENSITIVE_EQUALITY)
+ .containsNoneOf("GHI", "XYZ");
+ // We fail with the more helpful failure message about the unexpected value, not the NPE.
+ assertFailureKeys(
+ "expected not to contain any of",
+ "testing whether",
+ "but contained",
+ "corresponding to",
+ "---",
+ "full contents",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, GHI) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void containsNoneOf_handlesExceptions_alwaysFails() {
+ List<String> actual = asList("abc", null, "ghi");
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(CASE_INSENSITIVE_EQUALITY)
+ .containsNoneOf("DEF", "XYZ");
+ // The actual list does not contain the forbidden matcesh. However, we cannot establish that
+ // without hitting the null along the way, and that throws NPE, so we are contractually required
+ // to fail.
+ assertFailureKeys(
+ "one or more exceptions were thrown while comparing elements",
+ "first exception",
+ "expected not to contain any of",
+ "testing whether",
+ "found no matches (but failing because of exception)",
+ "full contents");
+ assertFailureValue("expected not to contain any of", "[DEF, XYZ]");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, DEF) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void containsNoneIn_success() {
+ ImmutableList<String> actual =
+ ImmutableList.of("+128", "+64", "This is not the string you're looking for", "0x40");
+ ImmutableList<Integer> excluded = ImmutableList.of(255, 256, 257);
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsNoneIn(excluded);
+ }
+
+ @Test
+ public void containsNoneIn_failure() {
+ ImmutableList<String> actual = ImmutableList.of("+128", "+64", "+256", "0x40");
+ ImmutableList<Integer> excluded = ImmutableList.of(255, 256, 257);
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsNoneIn(excluded);
+ assertFailureKeys(
+ "expected not to contain any of",
+ "testing whether",
+ "but contained",
+ "corresponding to",
+ "---",
+ "full contents");
+ assertFailureValue("but contained", "[+256]");
+ assertFailureValue("corresponding to", "256");
+ }
+
+ @Test
+ public void containsNoneIn_null() {
+ List<String> actual = asList("+128", "+64", null, "0x40");
+ List<Integer> excluded = asList(255, null, 257);
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsNoneIn(excluded);
+ assertFailureKeys(
+ "expected not to contain any of",
+ "testing whether",
+ "but contained",
+ "corresponding to",
+ "---",
+ "full contents");
+ assertFailureValue("but contained", "[null]");
+ assertFailureValue("corresponding to", "null");
+ }
+
+ @Test
+ public void containsNoneIn_array() {
+ ImmutableList<String> actual =
+ ImmutableList.of("+128", "+64", "This is not the string you're looking for", "0x40");
+ Integer[] excluded = new Integer[] {255, 256, 257};
+ assertThat(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsNoneIn(excluded);
+
+ excluded = new Integer[] {127, 128, 129};
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .comparingElementsUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsNoneIn(excluded);
+ assertFailureKeys(
+ "expected not to contain any of",
+ "testing whether",
+ "but contained",
+ "corresponding to",
+ "---",
+ "full contents");
+ assertFailureValue("but contained", "[+128]");
+ assertFailureValue("corresponding to", "128");
+ }
+
+ @Test
+ public void formattingDiffsUsing_success() {
+ ImmutableList<Record> actual =
+ ImmutableList.of(Record.create(3, 300), Record.create(2, 200), Record.create(1, 100));
+ assertThat(actual)
+ .formattingDiffsUsing(RECORD_DIFF_FORMATTER)
+ .displayingDiffsPairedBy(RECORD_ID)
+ .containsExactly(Record.create(1, 100), Record.create(2, 200), Record.create(3, 300));
+ }
+
+ @Test
+ public void formattingDiffsUsing_failure() {
+ ImmutableList<Record> actual =
+ ImmutableList.of(
+ Record.create(3, 300),
+ Record.create(2, 201),
+ Record.create(1, 100),
+ Record.create(2, 199));
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .formattingDiffsUsing(RECORD_DIFF_FORMATTER)
+ .displayingDiffsPairedBy(RECORD_ID)
+ .containsExactly(Record.create(1, 100), Record.create(2, 200), Record.create(3, 300));
+ assertFailureKeys(
+ "for key",
+ "missing",
+ "unexpected (2)",
+ "#1",
+ "diff",
+ "#2",
+ "diff",
+ "---",
+ "expected",
+ "but was");
+ assertFailureValue("missing", "2/200");
+ assertFailureValue("#1", "2/201");
+ assertFailureValueIndexed("diff", 0, "score:1");
+ assertFailureValue("#2", "2/199");
+ assertFailureValueIndexed("diff", 1, "score:-1");
+ }
+
+ private static final class CountsToStringCalls {
+ int calls;
+
+ @Override
+ public String toString() {
+ calls++;
+ return super.toString();
+ }
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/IterableSubjectTest.java b/core/src/test/java/com/google/common/truth/IterableSubjectTest.java
new file mode 100644
index 00000000..beab5ff7
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/IterableSubjectTest.java
@@ -0,0 +1,1212 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.lang.String.CASE_INSENSITIVE_ORDER;
+import static java.util.Arrays.asList;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Iterators;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.TreeSet;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link IterableSubject} APIs, excluding those that use {@link Correspondence} (which
+ * are tested in {@link IterableSubjectCorrespondenceTest}.
+ *
+ * @author David Saff
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+@RunWith(JUnit4.class)
+public class IterableSubjectTest extends BaseSubjectTestCase {
+
+ @Test
+ public void hasSize() {
+ assertThat(ImmutableList.of(1, 2, 3)).hasSize(3);
+ }
+
+ @Test
+ public void hasSizeZero() {
+ assertThat(ImmutableList.of()).hasSize(0);
+ }
+
+ @Test
+ public void hasSizeFails() {
+ expectFailureWhenTestingThat(ImmutableList.of(1, 2, 3)).hasSize(4);
+ assertFailureValue("value of", "iterable.size()");
+ }
+
+ @Test
+ public void hasSizeNegative() {
+ try {
+ assertThat(ImmutableList.of(1, 2, 3)).hasSize(-1);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test
+ public void iterableContains() {
+ assertThat(asList(1, 2, 3)).contains(1);
+ }
+
+ @Test
+ public void iterableContainsWithNull() {
+ assertThat(asList(1, null, 3)).contains(null);
+ }
+
+ @Test
+ public void iterableContainsFailsWithSameToString() {
+ expectFailureWhenTestingThat(asList(1L, 2L, 3L, 2L)).contains(2);
+ assertFailureKeys(
+ "expected to contain",
+ "an instance of",
+ "but did not",
+ "though it did contain",
+ "full contents");
+ assertFailureValue("expected to contain", "2");
+ assertFailureValue("an instance of", "java.lang.Integer");
+ assertFailureValue("though it did contain", "[2 [2 copies]] (java.lang.Long)");
+ assertFailureValue("full contents", "[1, 2, 3, 2]");
+ }
+
+ @Test
+ public void iterableContainsFailsWithSameToStringAndNull() {
+ expectFailureWhenTestingThat(asList(1, "null")).contains(null);
+ assertFailureValue("an instance of", "null type");
+ }
+
+ @Test
+ public void iterableContainsFailure() {
+ expectFailureWhenTestingThat(asList(1, 2, 3)).contains(5);
+ assertFailureKeys("expected to contain", "but was");
+ assertFailureValue("expected to contain", "5");
+ }
+
+ @Test
+ public void iterableDoesNotContain() {
+ assertThat(asList(1, null, 3)).doesNotContain(5);
+ }
+
+ @Test
+ public void iterableDoesNotContainNull() {
+ assertThat(asList(1, 2, 3)).doesNotContain(null);
+ }
+
+ @Test
+ public void iterableDoesNotContainFailure() {
+ expectFailureWhenTestingThat(asList(1, 2, 3)).doesNotContain(2);
+ assertFailureKeys("expected not to contain", "but was");
+ assertFailureValue("expected not to contain", "2");
+ }
+
+ @Test
+ public void doesNotContainDuplicates() {
+ assertThat(asList(1, 2, 3)).containsNoDuplicates();
+ }
+
+ @Test
+ public void doesNotContainDuplicatesMixedTypes() {
+ assertThat(asList(1, 2, 2L, 3)).containsNoDuplicates();
+ }
+
+ @Test
+ public void doesNotContainDuplicatesFailure() {
+ expectFailureWhenTestingThat(asList(1, 2, 2, 3)).containsNoDuplicates();
+ assertFailureKeys("expected not to contain duplicates", "but contained", "full contents");
+ assertFailureValue("but contained", "[2 x 2]");
+ assertFailureValue("full contents", "[1, 2, 2, 3]");
+ }
+
+ @Test
+ public void iterableContainsAnyOf() {
+ assertThat(asList(1, 2, 3)).containsAnyOf(1, 5);
+ }
+
+ @Test
+ public void iterableContainsAnyOfWithNull() {
+ assertThat(asList(1, null, 3)).containsAnyOf(null, 5);
+ }
+
+ @Test
+ public void iterableContainsAnyOfWithNullInThirdAndFinalPosition() {
+ assertThat(asList(1, null, 3)).containsAnyOf(4, 5, (Integer) null);
+ }
+
+ @Test
+ public void iterableContainsAnyOfFailure() {
+ expectFailureWhenTestingThat(asList(1, 2, 3)).containsAnyOf(5, 6, 0);
+ assertFailureKeys("expected to contain any of", "but was");
+ assertFailureValue("expected to contain any of", "[5, 6, 0]");
+ }
+
+ @Test
+ public void iterableContainsAnyOfFailsWithSameToStringAndHomogeneousList() {
+ expectFailureWhenTestingThat(asList(1L, 2L, 3L)).containsAnyOf(2, 3);
+ assertFailureKeys(
+ "expected to contain any of", "but did not", "though it did contain", "full contents");
+ assertFailureValue("expected to contain any of", "[2, 3] (java.lang.Integer)");
+ assertFailureValue("though it did contain", "[2, 3] (java.lang.Long)");
+ assertFailureValue("full contents", "[1, 2, 3]");
+ }
+
+ @Test
+ public void iterableContainsAnyOfFailsWithSameToStringAndHomogeneousListWithDuplicates() {
+ expectFailureWhenTestingThat(asList(3L, 3L)).containsAnyOf(2, 3, 3);
+ assertFailureKeys(
+ "expected to contain any of", "but did not", "though it did contain", "full contents");
+ assertFailureValue("expected to contain any of", "[2, 3 [2 copies]] (java.lang.Integer)");
+ assertFailureValue("though it did contain", "[3 [2 copies]] (java.lang.Long)");
+ assertFailureValue("full contents", "[3, 3]");
+ }
+
+ @Test
+ public void iterableContainsAnyOfFailsWithSameToStringAndNullInSubject() {
+ expectFailureWhenTestingThat(asList(null, "abc")).containsAnyOf("def", "null");
+ assertFailureKeys(
+ "expected to contain any of", "but did not", "though it did contain", "full contents");
+ assertFailureValue("expected to contain any of", "[def, null] (java.lang.String)");
+ assertFailureValue("though it did contain", "[null (null type)]");
+ assertFailureValue("full contents", "[null, abc]");
+ }
+
+ @Test
+ public void iterableContainsAnyOfFailsWithSameToStringAndNullInExpectation() {
+ expectFailureWhenTestingThat(asList("null", "abc")).containsAnyOf("def", null);
+ assertFailureKeys(
+ "expected to contain any of", "but did not", "though it did contain", "full contents");
+ assertFailureValue("expected to contain any of", "[def (java.lang.String), null (null type)]");
+ assertFailureValue("though it did contain", "[null] (java.lang.String)");
+ assertFailureValue("full contents", "[null, abc]");
+ }
+
+ @Test
+ public void iterableContainsAnyOfWithOneShotIterable() {
+ final Iterator<Object> iterator = asList((Object) 2, 1, "b").iterator();
+ Iterable<Object> iterable =
+ new Iterable<Object>() {
+ @Override
+ public Iterator<Object> iterator() {
+ return iterator;
+ }
+ };
+
+ assertThat(iterable).containsAnyOf(3, "a", 7, "b", 0);
+ }
+
+ @Test
+ public void iterableContainsAnyInIterable() {
+ assertThat(asList(1, 2, 3)).containsAnyIn(asList(1, 10, 100));
+
+ expectFailureWhenTestingThat(asList(1, 2, 3)).containsAnyIn(asList(5, 6, 0));
+ assertFailureKeys("expected to contain any of", "but was");
+ assertFailureValue("expected to contain any of", "[5, 6, 0]");
+ }
+
+ @Test
+ public void iterableContainsAnyInArray() {
+ assertThat(asList(1, 2, 3)).containsAnyIn(new Integer[] {1, 10, 100});
+
+ expectFailureWhenTestingThat(asList(1, 2, 3)).containsAnyIn(new Integer[] {5, 6, 0});
+ assertFailureKeys("expected to contain any of", "but was");
+ assertFailureValue("expected to contain any of", "[5, 6, 0]");
+ }
+
+ @Test
+ public void iterableContainsAtLeast() {
+ assertThat(asList(1, 2, 3)).containsAtLeast(1, 2);
+ }
+
+ @Test
+ public void iterableContainsAtLeastWithMany() {
+ assertThat(asList(1, 2, 3)).containsAtLeast(1, 2);
+ }
+
+ @Test
+ public void iterableContainsAtLeastWithDuplicates() {
+ assertThat(asList(1, 2, 2, 2, 3)).containsAtLeast(2, 2);
+ }
+
+ @Test
+ public void iterableContainsAtLeastWithNull() {
+ assertThat(asList(1, null, 3)).containsAtLeast(3, (Integer) null);
+ }
+
+ @Test
+ public void iterableContainsAtLeastWithNullAtThirdAndFinalPosition() {
+ assertThat(asList(1, null, 3)).containsAtLeast(1, 3, (Object) null);
+ }
+
+ /*
+ * Test that we only call toString() if the assertion fails -- that is, not just if the elements
+ * are out of order, but only if someone actually calls inOrder(). There are 2 reasons for this:
+ *
+ * 1. Calling toString() uses extra time and space. (To be fair, Iterable assertions often use a
+ * lot of those already.)
+ *
+ * 2. Some toString() methods are buggy. Arguably we shouldn't accommodate these, especially since
+ * those users are in for a nasty surprise if their tests actually fail someday, but I don't want
+ * to bite that off now. (Maybe Fact should catch exceptions from toString()?)
+ */
+ @Test
+ public void iterableContainsAtLeastElementsInOutOfOrderDoesNotStringify() {
+ CountsToStringCalls o = new CountsToStringCalls();
+ List<Object> actual = asList(o, 1);
+ List<Object> expected = asList(1, o);
+ assertThat(actual).containsAtLeastElementsIn(expected);
+ assertThat(o.calls).isEqualTo(0);
+ expectFailureWhenTestingThat(actual).containsAtLeastElementsIn(expected).inOrder();
+ assertThat(o.calls).isGreaterThan(0);
+ }
+
+ @Test
+ public void iterableContainsAtLeastFailure() {
+ expectFailureWhenTestingThat(asList(1, 2, 3)).containsAtLeast(1, 2, 4);
+ assertFailureKeys("missing (1)", "---", "expected to contain at least", "but was");
+ assertFailureValue("missing (1)", "4");
+ assertFailureValue("expected to contain at least", "[1, 2, 4]");
+ }
+
+ @Test
+ public void iterableContainsAtLeastWithExtras() {
+ expectFailureWhenTestingThat(asList("y", "x")).containsAtLeast("x", "y", "z");
+ assertFailureValue("missing (1)", "z");
+ }
+
+ @Test
+ public void iterableContainsAtLeastWithExtraCopiesOfOutOfOrder() {
+ expectFailureWhenTestingThat(asList("y", "x")).containsAtLeast("x", "y", "y");
+ assertFailureValue("missing (1)", "y");
+ }
+
+ @Test
+ public void iterableContainsAtLeastWithDuplicatesFailure() {
+ expectFailureWhenTestingThat(asList(1, 2, 3)).containsAtLeast(1, 2, 2, 2, 3, 4);
+ assertFailureValue("missing (3)", "2 [2 copies], 4");
+ }
+
+ /*
+ * Slightly subtle test to ensure that if multiple equal elements are found
+ * to be missing we only reference it once in the output message.
+ */
+ @Test
+ public void iterableContainsAtLeastWithDuplicateMissingElements() {
+ expectFailureWhenTestingThat(asList(1, 2)).containsAtLeast(4, 4, 4);
+ assertFailureValue("missing (3)", "4 [3 copies]");
+ }
+
+ @Test
+ public void iterableContainsAtLeastWithNullFailure() {
+ expectFailureWhenTestingThat(asList(1, null, 3)).containsAtLeast(1, null, null, 3);
+ assertFailureValue("missing (1)", "null");
+ }
+
+ @Test
+ public void iterableContainsAtLeastFailsWithSameToStringAndHomogeneousList() {
+ expectFailureWhenTestingThat(asList(1L, 2L)).containsAtLeast(1, 2);
+ assertFailureValue("missing (2)", "1, 2 (java.lang.Integer)");
+ assertFailureValue("though it did contain (2)", "1, 2 (java.lang.Long)");
+ }
+
+ @Test
+ public void iterableContainsAtLeastFailsWithSameToStringAndHomogeneousListWithDuplicates() {
+ expectFailureWhenTestingThat(asList(1L, 2L, 2L)).containsAtLeast(1, 1, 2);
+ assertFailureValue("missing (3)", "1 [2 copies], 2 (java.lang.Integer)");
+ assertFailureValue("though it did contain (3)", "1, 2 [2 copies] (java.lang.Long)");
+ }
+
+ @Test
+ public void iterableContainsAtLeastFailsWithSameToStringAndHomogeneousListWithNull() {
+ expectFailureWhenTestingThat(asList("null", "abc")).containsAtLeast("abc", null);
+ assertFailureValue("missing (1)", "null (null type)");
+ assertFailureValue("though it did contain (1)", "null (java.lang.String)");
+ }
+
+ @Test
+ public void iterableContainsAtLeastFailsWithSameToStringAndHeterogeneousListWithDuplicates() {
+ expectFailureWhenTestingThat(asList(1, 2, 2L, 3L, 3L)).containsAtLeast(2L, 2L, 3, 3);
+ assertFailureValue("missing (3)", "2 (java.lang.Long), 3 (java.lang.Integer) [2 copies]");
+ assertFailureValue(
+ "though it did contain (3)", "2 (java.lang.Integer), 3 (java.lang.Long) [2 copies]");
+ }
+
+ @Test
+ public void iterableContainsAtLeastFailsWithEmptyString() {
+ expectFailureWhenTestingThat(asList("a", null)).containsAtLeast("", null);
+
+ assertFailureKeys("missing (1)", "---", "expected to contain at least", "but was");
+ assertFailureValue("missing (1)", "");
+ }
+
+ @Test
+ public void iterableContainsAtLeastInOrder() {
+ assertThat(asList(3, 2, 5)).containsAtLeast(3, 2, 5).inOrder();
+ }
+
+ @Test
+ public void iterableContainsAtLeastInOrderWithGaps() {
+ assertThat(asList(3, 2, 5)).containsAtLeast(3, 5).inOrder();
+ assertThat(asList(3, 2, 2, 4, 5)).containsAtLeast(3, 2, 2, 5).inOrder();
+ assertThat(asList(3, 1, 4, 1, 5)).containsAtLeast(3, 1, 5).inOrder();
+ assertThat(asList("x", "y", "y", "z")).containsAtLeast("x", "y", "z").inOrder();
+ assertThat(asList("x", "x", "y", "z")).containsAtLeast("x", "y", "z").inOrder();
+ assertThat(asList("z", "x", "y", "z")).containsAtLeast("x", "y", "z").inOrder();
+ assertThat(asList("x", "x", "y", "z", "x")).containsAtLeast("x", "y", "z", "x").inOrder();
+ }
+
+ @Test
+ public void iterableContainsAtLeastInOrderWithNull() {
+ assertThat(asList(3, null, 5)).containsAtLeast(3, null, 5).inOrder();
+ assertThat(asList(3, null, 7, 5)).containsAtLeast(3, null, 5).inOrder();
+ }
+
+ @Test
+ public void iterableContainsAtLeastInOrderWithFailure() {
+ expectFailureWhenTestingThat(asList(1, null, 3)).containsAtLeast(null, 1, 3).inOrder();
+ assertFailureKeys(
+ "required elements were all found, but order was wrong",
+ "expected order for required elements",
+ "but was");
+ assertFailureValue("expected order for required elements", "[null, 1, 3]");
+ }
+
+ @Test
+ public void iterableContainsAtLeastInOrderWithOneShotIterable() {
+ final Iterable<Object> iterable = Arrays.<Object>asList(2, 1, null, 4, "a", 3, "b");
+ final Iterator<Object> iterator = iterable.iterator();
+ Iterable<Object> oneShot =
+ new Iterable<Object>() {
+ @Override
+ public Iterator<Object> iterator() {
+ return iterator;
+ }
+
+ @Override
+ public String toString() {
+ return Iterables.toString(iterable);
+ }
+ };
+
+ assertThat(oneShot).containsAtLeast(1, null, 3).inOrder();
+ }
+
+ @Test
+ public void iterableContainsAtLeastInOrderWithOneShotIterableWrongOrder() {
+ final Iterator<Object> iterator = asList((Object) 2, 1, null, 4, "a", 3, "b").iterator();
+ Iterable<Object> iterable =
+ new Iterable<Object>() {
+ @Override
+ public Iterator<Object> iterator() {
+ return iterator;
+ }
+
+ @Override
+ public String toString() {
+ return "BadIterable";
+ }
+ };
+
+ expectFailureWhenTestingThat(iterable).containsAtLeast(1, 3, (Object) null).inOrder();
+ assertFailureKeys(
+ "required elements were all found, but order was wrong",
+ "expected order for required elements",
+ "but was");
+ assertFailureValue("expected order for required elements", "[1, 3, null]");
+ }
+
+ @Test
+ public void iterableContainsAtLeastInOrderWrongOrderAndMissing() {
+ expectFailureWhenTestingThat(asList(1, 2)).containsAtLeast(2, 1, 3).inOrder();
+ }
+
+ @Test
+ public void iterableContainsAtLeastElementsInIterable() {
+ assertThat(asList(1, 2, 3)).containsAtLeastElementsIn(asList(1, 2));
+
+ expectFailureWhenTestingThat(asList(1, 2, 3)).containsAtLeastElementsIn(asList(1, 2, 4));
+ assertFailureKeys("missing (1)", "---", "expected to contain at least", "but was");
+ assertFailureValue("missing (1)", "4");
+ assertFailureValue("expected to contain at least", "[1, 2, 4]");
+ }
+
+ @Test
+ public void iterableContainsAtLeastElementsInCanUseFactPerElement() {
+ expectFailureWhenTestingThat(asList("abc"))
+ .containsAtLeastElementsIn(asList("123\n456", "789"));
+ assertFailureKeys("missing (2)", "#1", "#2", "---", "expected to contain at least", "but was");
+ assertFailureValue("#1", "123\n456");
+ assertFailureValue("#2", "789");
+ }
+
+ @Test
+ public void iterableContainsAtLeastElementsInArray() {
+ assertThat(asList(1, 2, 3)).containsAtLeastElementsIn(new Integer[] {1, 2});
+
+ expectFailureWhenTestingThat(asList(1, 2, 3))
+ .containsAtLeastElementsIn(new Integer[] {1, 2, 4});
+ assertFailureKeys("missing (1)", "---", "expected to contain at least", "but was");
+ assertFailureValue("missing (1)", "4");
+ assertFailureValue("expected to contain at least", "[1, 2, 4]");
+ }
+
+ @Test
+ public void iterableContainsNoneOf() {
+ assertThat(asList(1, 2, 3)).containsNoneOf(4, 5, 6);
+ }
+
+ @Test
+ public void iterableContainsNoneOfFailure() {
+ expectFailureWhenTestingThat(asList(1, 2, 3)).containsNoneOf(1, 2, 4);
+ assertFailureKeys("expected not to contain any of", "but contained", "full contents");
+ assertFailureValue("expected not to contain any of", "[1, 2, 4]");
+ assertFailureValue("but contained", "[1, 2]");
+ assertFailureValue("full contents", "[1, 2, 3]");
+ }
+
+ @Test
+ public void iterableContainsNoneOfFailureWithDuplicateInSubject() {
+ expectFailureWhenTestingThat(asList(1, 2, 2, 3)).containsNoneOf(1, 2, 4);
+ assertFailureValue("but contained", "[1, 2]");
+ }
+
+ @Test
+ public void iterableContainsNoneOfFailureWithDuplicateInExpected() {
+ expectFailureWhenTestingThat(asList(1, 2, 3)).containsNoneOf(1, 2, 2, 4);
+ assertFailureValue("but contained", "[1, 2]");
+ }
+
+ @Test
+ public void iterableContainsNoneOfFailureWithEmptyString() {
+ expectFailureWhenTestingThat(asList("")).containsNoneOf("", null);
+ assertFailureKeys("expected not to contain any of", "but contained", "full contents");
+ assertFailureValue("expected not to contain any of", "[\"\" (empty String), null]");
+ assertFailureValue("but contained", "[\"\" (empty String)]");
+ assertFailureValue("full contents", "[]");
+ }
+
+ @Test
+ public void iterableContainsNoneInIterable() {
+ assertThat(asList(1, 2, 3)).containsNoneIn(asList(4, 5, 6));
+ expectFailureWhenTestingThat(asList(1, 2, 3)).containsNoneIn(asList(1, 2, 4));
+ assertFailureKeys("expected not to contain any of", "but contained", "full contents");
+ assertFailureValue("expected not to contain any of", "[1, 2, 4]");
+ assertFailureValue("but contained", "[1, 2]");
+ assertFailureValue("full contents", "[1, 2, 3]");
+ }
+
+ @Test
+ public void iterableContainsNoneInArray() {
+ assertThat(asList(1, 2, 3)).containsNoneIn(new Integer[] {4, 5, 6});
+ expectFailureWhenTestingThat(asList(1, 2, 3)).containsNoneIn(new Integer[] {1, 2, 4});
+ }
+
+ @Test
+ public void iterableContainsExactlyArray() {
+ String[] stringArray = {"a", "b"};
+ ImmutableList<String[]> iterable = ImmutableList.of(stringArray);
+ // This test fails w/o the explicit cast
+ assertThat(iterable).containsExactly((Object) stringArray);
+ }
+
+ @Test
+ public void arrayContainsExactly() {
+ ImmutableList<String> iterable = ImmutableList.of("a", "b");
+ String[] array = {"a", "b"};
+ assertThat(iterable).containsExactly((Object[]) array);
+ }
+
+ @Test
+ public void iterableContainsExactlyWithMany() {
+ assertThat(asList(1, 2, 3)).containsExactly(1, 2, 3);
+ }
+
+ @Test
+ public void iterableContainsExactlyOutOfOrder() {
+ assertThat(asList(1, 2, 3, 4)).containsExactly(3, 1, 4, 2);
+ }
+
+ @Test
+ public void iterableContainsExactlyWithDuplicates() {
+ assertThat(asList(1, 2, 2, 2, 3)).containsExactly(1, 2, 2, 2, 3);
+ }
+
+ @Test
+ public void iterableContainsExactlyWithDuplicatesOutOfOrder() {
+ assertThat(asList(1, 2, 2, 2, 3)).containsExactly(2, 1, 2, 3, 2);
+ }
+
+ @Test
+ public void iterableContainsExactlyWithOnlyNullPassedAsNullArray() {
+ // Truth is tolerant of this erroneous varargs call.
+ Iterable<Object> actual = asList((Object) null);
+ assertThat(actual).containsExactly((Object[]) null);
+ }
+
+ @Test
+ public void iterableContainsExactlyWithOnlyNull() {
+ Iterable<Object> actual = asList((Object) null);
+ assertThat(actual).containsExactly((Object) null);
+ }
+
+ @Test
+ public void iterableContainsExactlyWithNullSecond() {
+ assertThat(asList(1, null)).containsExactly(1, null);
+ }
+
+ @Test
+ public void iterableContainsExactlyWithNullThird() {
+ assertThat(asList(1, 2, null)).containsExactly(1, 2, null);
+ }
+
+ @Test
+ public void iterableContainsExactlyWithNull() {
+ assertThat(asList(1, null, 3)).containsExactly(1, null, 3);
+ }
+
+ @Test
+ public void iterableContainsExactlyWithNullOutOfOrder() {
+ assertThat(asList(1, null, 3)).containsExactly(1, 3, (Integer) null);
+ }
+
+ @Test
+ public void iterableContainsExactlyOutOfOrderDoesNotStringify() {
+ CountsToStringCalls o = new CountsToStringCalls();
+ List<Object> actual = asList(o, 1);
+ List<Object> expected = asList(1, o);
+ assertThat(actual).containsExactlyElementsIn(expected);
+ assertThat(o.calls).isEqualTo(0);
+ expectFailureWhenTestingThat(actual).containsExactlyElementsIn(expected).inOrder();
+ assertThat(o.calls).isGreaterThan(0);
+ }
+
+ @Test
+ public void iterableContainsExactlyWithEmptyString() {
+ expectFailureWhenTestingThat(asList()).containsExactly("");
+
+ assertFailureValue("missing (1)", "");
+ }
+
+ @Test
+ public void iterableContainsExactlyWithEmptyStringAndUnexpectedItem() {
+ expectFailureWhenTestingThat(asList("a", null)).containsExactly("");
+
+ assertFailureKeys("missing (1)", "unexpected (2)", "---", "expected", "but was");
+ assertFailureValue("missing (1)", "");
+ assertFailureValue("unexpected (2)", "a, null");
+ }
+
+ @Test
+ public void iterableContainsExactlyWithEmptyStringAndMissingItem() {
+ expectFailureWhenTestingThat(asList("")).containsExactly("a", null);
+
+ assertFailureValue("missing (2)", "a, null");
+ assertFailureValue("unexpected (1)", "");
+ }
+
+ @Test
+ public void iterableContainsExactlyWithEmptyStringAmongMissingItems() {
+ expectFailureWhenTestingThat(asList("a")).containsExactly("", "b");
+
+ assertFailureKeys(
+ "missing (2)", "#1", "#2", "", "unexpected (1)", "#1", "---", "expected", "but was");
+ assertFailureValueIndexed("#1", 0, "");
+ assertFailureValueIndexed("#2", 0, "b");
+ assertFailureValueIndexed("#1", 1, "a");
+ }
+
+ @Test
+ public void iterableContainsExactlySingleElement() {
+ assertThat(asList(1)).containsExactly(1);
+
+ expectFailureWhenTestingThat(asList(1)).containsExactly(2);
+ assertFailureKeys("value of", "expected", "but was");
+ assertFailureValue("value of", "iterable.onlyElement()");
+ }
+
+ @Test
+ public void iterableContainsExactlySingleElementNoEqualsMagic() {
+ expectFailureWhenTestingThat(asList(1)).containsExactly(1L);
+ assertFailureValueIndexed("an instance of", 0, "java.lang.Long");
+ }
+
+ @Test
+ public void iterableContainsExactlyWithElementsThatThrowWhenYouCallHashCode() {
+ HashCodeThrower one = new HashCodeThrower();
+ HashCodeThrower two = new HashCodeThrower();
+
+ assertThat(asList(one, two)).containsExactly(two, one);
+ assertThat(asList(one, two)).containsExactly(one, two).inOrder();
+ assertThat(asList(one, two)).containsExactlyElementsIn(asList(two, one));
+ assertThat(asList(one, two)).containsExactlyElementsIn(asList(one, two)).inOrder();
+
+ expectFailureWhenTestingThat(asList(one, two)).containsExactly(one);
+ }
+
+ private static class HashCodeThrower {
+ @Override
+ public boolean equals(Object other) {
+ return this == other;
+ }
+
+ @Override
+ public int hashCode() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String toString() {
+ return "HCT";
+ }
+ }
+
+ @Test
+ public void iterableContainsExactlyElementsInInOrderPassesWithEmptyExpectedAndActual() {
+ assertThat(ImmutableList.of()).containsExactlyElementsIn(ImmutableList.of()).inOrder();
+ }
+
+ @Test
+ public void iterableContainsExactlyElementsInWithEmptyExpected() {
+ expectFailureWhenTestingThat(asList("foo")).containsExactlyElementsIn(ImmutableList.of());
+ assertFailureKeys("expected to be empty", "but was");
+ }
+
+ @Test
+ public void iterableContainsExactlyElementsInErrorMessageIsInOrder() {
+ expectFailureWhenTestingThat(asList("foo OR bar"))
+ .containsExactlyElementsIn(asList("foo", "bar"));
+ assertFailureValue("missing (2)", "foo, bar");
+ assertFailureValue("unexpected (1)", "foo OR bar");
+ }
+
+ @Test
+ public void iterableContainsExactlyMissingItemFailure() {
+ expectFailureWhenTestingThat(asList(1, 2)).containsExactly(1, 2, 4);
+ assertFailureValue("missing (1)", "4");
+ }
+
+ @Test
+ public void iterableContainsExactlyUnexpectedItemFailure() {
+ expectFailureWhenTestingThat(asList(1, 2, 3)).containsExactly(1, 2);
+ assertFailureValue("unexpected (1)", "3");
+ }
+
+ @Test
+ public void iterableContainsExactlyWithDuplicatesNotEnoughItemsFailure() {
+ expectFailureWhenTestingThat(asList(1, 2, 3)).containsExactly(1, 2, 2, 2, 3);
+ assertFailureValue("missing (2)", "2 [2 copies]");
+ }
+
+ @Test
+ public void iterableContainsExactlyWithDuplicatesMissingItemFailure() {
+ expectFailureWhenTestingThat(asList(1, 2, 3)).containsExactly(1, 2, 2, 2, 3, 4);
+ assertFailureValue("missing (3)", "2 [2 copies], 4");
+ }
+
+ @Test
+ public void iterableContainsExactlyWithDuplicatesMissingItemsWithNewlineFailure() {
+ expectFailureWhenTestingThat(asList("a", "b", "foo\nbar"))
+ .containsExactly("a", "b", "foo\nbar", "foo\nbar", "foo\nbar");
+ assertFailureKeys("missing (2)", "#1 [2 copies]", "---", "expected", "but was");
+ assertFailureValue("#1 [2 copies]", "foo\nbar");
+ }
+
+ @Test
+ public void iterableContainsExactlyWithDuplicatesMissingAndExtraItemsWithNewlineFailure() {
+ expectFailureWhenTestingThat(asList("a\nb", "a\nb")).containsExactly("foo\nbar", "foo\nbar");
+ assertFailureKeys(
+ "missing (2)",
+ "#1 [2 copies]",
+ "",
+ "unexpected (2)",
+ "#1 [2 copies]",
+ "---",
+ "expected",
+ "but was");
+ assertFailureValueIndexed("#1 [2 copies]", 0, "foo\nbar");
+ assertFailureValueIndexed("#1 [2 copies]", 1, "a\nb");
+ }
+
+ @Test
+ public void iterableContainsExactlyWithDuplicatesUnexpectedItemFailure() {
+ expectFailureWhenTestingThat(asList(1, 2, 2, 2, 2, 3)).containsExactly(1, 2, 2, 3);
+ assertFailureValue("unexpected (2)", "2 [2 copies]");
+ }
+
+ /*
+ * Slightly subtle test to ensure that if multiple equal elements are found
+ * to be missing we only reference it once in the output message.
+ */
+ @Test
+ public void iterableContainsExactlyWithDuplicateMissingElements() {
+ expectFailureWhenTestingThat(asList()).containsExactly(4, 4, 4);
+ assertFailureValue("missing (3)", "4 [3 copies]");
+ }
+
+ @Test
+ public void iterableContainsExactlyWithNullFailure() {
+ expectFailureWhenTestingThat(asList(1, null, 3)).containsExactly(1, null, null, 3);
+ assertFailureValue("missing (1)", "null");
+ }
+
+ @Test
+ public void iterableContainsExactlyWithMissingAndExtraElements() {
+ expectFailureWhenTestingThat(asList(1, 2, 3)).containsExactly(1, 2, 4);
+ assertFailureValue("missing (1)", "4");
+ assertFailureValue("unexpected (1)", "3");
+ }
+
+ @Test
+ public void iterableContainsExactlyWithDuplicateMissingAndExtraElements() {
+ expectFailureWhenTestingThat(asList(1, 2, 3, 3)).containsExactly(1, 2, 4, 4);
+ assertFailureValue("missing (2)", "4 [2 copies]");
+ assertFailureValue("unexpected (2)", "3 [2 copies]");
+ }
+
+ @Test
+ public void iterableContainsExactlyWithCommaSeparatedVsIndividual() {
+ expectFailureWhenTestingThat(asList("a, b")).containsExactly("a", "b");
+ assertFailureKeys(
+ "missing (2)", "#1", "#2", "", "unexpected (1)", "#1", "---", "expected", "but was");
+ assertFailureValueIndexed("#1", 0, "a");
+ assertFailureValueIndexed("#2", 0, "b");
+ assertFailureValueIndexed("#1", 1, "a, b");
+ }
+
+ @Test
+ public void iterableContainsExactlyFailsWithSameToStringAndHomogeneousList() {
+ expectFailureWhenTestingThat(asList(1L, 2L)).containsExactly(1, 2);
+ assertFailureValue("missing (2)", "1, 2 (java.lang.Integer)");
+ assertFailureValue("unexpected (2)", "1, 2 (java.lang.Long)");
+ }
+
+ @Test
+ public void iterableContainsExactlyFailsWithSameToStringAndListWithNull() {
+ expectFailureWhenTestingThat(asList(1L, 2L)).containsExactly(null, 1, 2);
+ assertFailureValue(
+ "missing (3)", "null (null type), 1 (java.lang.Integer), 2 (java.lang.Integer)");
+ assertFailureValue("unexpected (2)", "1, 2 (java.lang.Long)");
+ }
+
+ @Test
+ public void iterableContainsExactlyFailsWithSameToStringAndHeterogeneousList() {
+ expectFailureWhenTestingThat(asList(1L, 2)).containsExactly(1, null, 2L);
+ assertFailureValue(
+ "missing (3)", "1 (java.lang.Integer), null (null type), 2 (java.lang.Long)");
+ assertFailureValue("unexpected (2)", "1 (java.lang.Long), 2 (java.lang.Integer)");
+ }
+
+ @Test
+ public void iterableContainsExactlyFailsWithSameToStringAndHomogeneousListWithDuplicates() {
+ expectFailureWhenTestingThat(asList(1L, 2L)).containsExactly(1, 2, 2);
+ assertFailureValue("missing (3)", "1, 2 [2 copies] (java.lang.Integer)");
+ assertFailureValue("unexpected (2)", "1, 2 (java.lang.Long)");
+ }
+
+ @Test
+ public void iterableContainsExactlyFailsWithSameToStringAndHeterogeneousListWithDuplicates() {
+ expectFailureWhenTestingThat(asList(1L, 2)).containsExactly(1, null, null, 2L, 2L);
+ assertFailureValue(
+ "missing (5)",
+ "1 (java.lang.Integer), null (null type) [2 copies], 2 (java.lang.Long) [2 copies]");
+ assertFailureValue("unexpected (2)", "1 (java.lang.Long), 2 (java.lang.Integer)");
+ }
+
+ @Test
+ public void iterableContainsExactlyWithOneIterableGivesWarning() {
+ expectFailureWhenTestingThat(asList(1, 2, 3, 4)).containsExactly(asList(1, 2, 3, 4));
+ assertThat(expectFailure.getFailure())
+ .hasMessageThat()
+ .contains(CONTAINS_EXACTLY_ITERABLE_WARNING);
+ }
+
+ @Test
+ public void iterableContainsExactlyElementsInWithOneIterableDoesNotGiveWarning() {
+ expectFailureWhenTestingThat(asList(1, 2, 3, 4)).containsExactlyElementsIn(asList(1, 2, 3));
+ assertFailureValue("unexpected (1)", "4");
+ }
+
+ @Test
+ public void iterableContainsExactlyWithTwoIterableDoesNotGivesWarning() {
+ expectFailureWhenTestingThat(asList(1, 2, 3, 4)).containsExactly(asList(1, 2), asList(3, 4));
+ assertThat(expectFailure.getFailure())
+ .hasMessageThat()
+ .doesNotContain(CONTAINS_EXACTLY_ITERABLE_WARNING);
+ }
+
+ private static final String CONTAINS_EXACTLY_ITERABLE_WARNING =
+ "Passing an iterable to the varargs method containsExactly(Object...) is "
+ + "often not the correct thing to do. Did you mean to call "
+ + "containsExactlyElementsIn(Iterable) instead?";
+
+ @Test
+ public void iterableContainsExactlyWithOneNonIterableDoesNotGiveWarning() {
+ expectFailureWhenTestingThat(asList(1, 2, 3, 4)).containsExactly(1);
+ assertFailureValue("unexpected (3)", "2, 3, 4");
+ }
+
+ @Test
+ public void iterableContainsExactlyInOrder() {
+ assertThat(asList(3, 2, 5)).containsExactly(3, 2, 5).inOrder();
+ }
+
+ @Test
+ public void iterableContainsExactlyInOrderWithNull() {
+ assertThat(asList(3, null, 5)).containsExactly(3, null, 5).inOrder();
+ }
+
+ @Test
+ public void iterableContainsExactlyInOrderWithFailure() {
+ expectFailureWhenTestingThat(asList(1, null, 3)).containsExactly(null, 1, 3).inOrder();
+ assertFailureKeys("contents match, but order was wrong", "expected", "but was");
+ assertFailureValue("expected", "[null, 1, 3]");
+ }
+
+ @Test
+ public void iterableContainsExactlyInOrderWithOneShotIterable() {
+ final Iterator<Object> iterator = asList((Object) 1, null, 3).iterator();
+ Iterable<Object> iterable =
+ new Iterable<Object>() {
+ @Override
+ public Iterator<Object> iterator() {
+ return iterator;
+ }
+ };
+ assertThat(iterable).containsExactly(1, null, 3).inOrder();
+ }
+
+ @Test
+ public void iterableContainsExactlyInOrderWithOneShotIterableWrongOrder() {
+ final Iterator<Object> iterator = asList((Object) 1, null, 3).iterator();
+ Iterable<Object> iterable =
+ new Iterable<Object>() {
+ @Override
+ public Iterator<Object> iterator() {
+ return iterator;
+ }
+
+ @Override
+ public String toString() {
+ return "BadIterable";
+ }
+ };
+
+ expectFailureWhenTestingThat(iterable).containsExactly(1, 3, null).inOrder();
+ assertFailureKeys("contents match, but order was wrong", "expected", "but was");
+ assertFailureValue("expected", "[1, 3, null]");
+ }
+
+ @Test
+ public void iterableWithNoToStringOverride() {
+ Iterable<Integer> iterable =
+ new Iterable<Integer>() {
+ @Override
+ public Iterator<Integer> iterator() {
+ return Iterators.forArray(1, 2, 3);
+ }
+ };
+
+ expectFailureWhenTestingThat(iterable).containsExactly(1, 2).inOrder();
+ assertFailureValue("but was", "[1, 2, 3]");
+ }
+
+ @Test
+ public void iterableContainsExactlyElementsInIterable() {
+ assertThat(asList(1, 2)).containsExactlyElementsIn(asList(1, 2));
+
+ expectFailureWhenTestingThat(asList(1, 2)).containsExactlyElementsIn(asList(1, 2, 4));
+ assertFailureValue("missing (1)", "4");
+ }
+
+ @Test
+ public void iterableContainsExactlyElementsInArray() {
+ assertThat(asList(1, 2)).containsExactlyElementsIn(new Integer[] {1, 2});
+
+ expectFailureWhenTestingThat(asList(1, 2)).containsExactlyElementsIn(new Integer[] {1, 2, 4});
+ assertFailureValue("missing (1)", "4");
+ }
+
+ @Test
+ public void nullEqualToNull() {
+ assertThat((Iterable<?>) null).isEqualTo(null);
+ }
+
+ @Test
+ public void nullEqualToSomething() {
+ expectFailureWhenTestingThat(null).isEqualTo(ImmutableList.of());
+ }
+
+ @Test
+ public void somethingEqualToNull() {
+ expectFailureWhenTestingThat(ImmutableList.of()).isEqualTo(null);
+ }
+
+ @Test
+ public void somethingEqualToSomething() {
+ expectFailureWhenTestingThat(ImmutableList.of()).isEqualTo(ImmutableList.of("a"));
+ // isEqualTo uses the containsExactly style of message:
+ assertFailureValue("missing (1)", "a");
+ }
+
+ @Test
+ public void isEqualToNotConsistentWithEquals() {
+ TreeSet<String> actual = new TreeSet<>(CASE_INSENSITIVE_ORDER);
+ TreeSet<String> expected = new TreeSet<>(CASE_INSENSITIVE_ORDER);
+ actual.add("one");
+ expected.add("ONE");
+ /*
+ * Our contract doesn't guarantee that the following test will pass. It *currently* does,
+ * though, and if we change that behavior, we want this test to let us know.
+ */
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ public void isEqualToNotConsistentWithEquals_failure() {
+ TreeSet<String> actual = new TreeSet<>(CASE_INSENSITIVE_ORDER);
+ TreeSet<String> expected = new TreeSet<>(CASE_INSENSITIVE_ORDER);
+ actual.add("one");
+ expected.add("ONE");
+ actual.add("two");
+ expectFailureWhenTestingThat(actual).isEqualTo(expected);
+ // The exact message generated is unspecified.
+ }
+
+ @Test
+ public void iterableIsEmpty() {
+ assertThat(asList()).isEmpty();
+ }
+
+ @Test
+ public void iterableIsEmptyWithFailure() {
+ expectFailureWhenTestingThat(asList(1, null, 3)).isEmpty();
+ assertFailureKeys("expected to be empty", "but was");
+ }
+
+ @Test
+ public void iterableIsNotEmpty() {
+ assertThat(asList("foo")).isNotEmpty();
+ }
+
+ @Test
+ public void iterableIsNotEmptyWithFailure() {
+ expectFailureWhenTestingThat(asList()).isNotEmpty();
+ assertFailureKeys("expected not to be empty");
+ }
+
+ @Test
+ public void iterableIsInStrictOrder() {
+ assertThat(asList()).isInStrictOrder();
+ assertThat(asList(1)).isInStrictOrder();
+ assertThat(asList(1, 2, 3, 4)).isInStrictOrder();
+ }
+
+ @Test
+ public void isInStrictOrderFailure() {
+ expectFailureWhenTestingThat(asList(1, 2, 2, 4)).isInStrictOrder();
+ assertFailureKeys(
+ "expected to be in strict order", "but contained", "followed by", "full contents");
+ assertFailureValue("but contained", "2");
+ assertFailureValue("followed by", "2");
+ assertFailureValue("full contents", "[1, 2, 2, 4]");
+ }
+
+ @Test
+ public void isInStrictOrderWithNonComparableElementsFailure() {
+ try {
+ assertThat(asList((Object) 1, "2", 3, "4")).isInStrictOrder();
+ fail("Should have thrown.");
+ } catch (ClassCastException expected) {
+ }
+ }
+
+ @Test
+ public void iterableIsInOrder() {
+ assertThat(asList()).isInOrder();
+ assertThat(asList(1)).isInOrder();
+ assertThat(asList(1, 1, 2, 3, 3, 3, 4)).isInOrder();
+ }
+
+ @Test
+ public void isInOrderFailure() {
+ expectFailureWhenTestingThat(asList(1, 3, 2, 4)).isInOrder();
+ assertFailureKeys("expected to be in order", "but contained", "followed by", "full contents");
+ assertFailureValue("but contained", "3");
+ assertFailureValue("followed by", "2");
+ assertFailureValue("full contents", "[1, 3, 2, 4]");
+ }
+
+ @Test
+ public void isInOrderMultipleFailures() {
+ expectFailureWhenTestingThat(asList(1, 3, 2, 4, 0)).isInOrder();
+ }
+
+ @Test
+ public void isInOrderWithNonComparableElementsFailure() {
+ try {
+ assertThat(asList((Object) 1, "2", 2, "3")).isInOrder();
+ fail("Should have thrown.");
+ } catch (ClassCastException expected) {
+ }
+ }
+
+ @Test
+ public void iterableIsInStrictOrderWithComparator() {
+ Iterable<String> emptyStrings = asList();
+ assertThat(emptyStrings).isInStrictOrder(COMPARE_AS_DECIMAL);
+ assertThat(asList("1")).isInStrictOrder(COMPARE_AS_DECIMAL);
+ // Note: Use "10" and "20" to distinguish numerical and lexicographical ordering.
+ assertThat(asList("1", "2", "10", "20")).isInStrictOrder(COMPARE_AS_DECIMAL);
+ }
+
+ @Test
+ public void iterableIsInStrictOrderWithComparatorFailure() {
+ expectFailureWhenTestingThat(asList("1", "2", "2", "10")).isInStrictOrder(COMPARE_AS_DECIMAL);
+ assertFailureKeys(
+ "expected to be in strict order", "but contained", "followed by", "full contents");
+ assertFailureValue("but contained", "2");
+ assertFailureValue("followed by", "2");
+ assertFailureValue("full contents", "[1, 2, 2, 10]");
+ }
+
+ @Test
+ public void iterableIsInOrderWithComparator() {
+ Iterable<String> emptyStrings = asList();
+ assertThat(emptyStrings).isInOrder(COMPARE_AS_DECIMAL);
+ assertThat(asList("1")).isInOrder(COMPARE_AS_DECIMAL);
+ assertThat(asList("1", "1", "2", "10", "10", "10", "20")).isInOrder(COMPARE_AS_DECIMAL);
+ }
+
+ @Test
+ public void iterableIsInOrderWithComparatorFailure() {
+ expectFailureWhenTestingThat(asList("1", "10", "2", "20")).isInOrder(COMPARE_AS_DECIMAL);
+ assertFailureKeys("expected to be in order", "but contained", "followed by", "full contents");
+ assertFailureValue("but contained", "10");
+ assertFailureValue("followed by", "2");
+ assertFailureValue("full contents", "[1, 10, 2, 20]");
+ }
+
+ private static final Comparator<String> COMPARE_AS_DECIMAL =
+ new Comparator<String>() {
+ @Override
+ public int compare(String a, String b) {
+ return Integer.valueOf(a).compareTo(Integer.valueOf(b));
+ }
+ };
+
+ private static class Foo {
+ private final int x;
+
+ private Foo(int x) {
+ this.x = x;
+ }
+ }
+
+ private static class Bar extends Foo {
+ private Bar(int x) {
+ super(x);
+ }
+ }
+
+ private static final Comparator<Foo> FOO_COMPARATOR =
+ new Comparator<Foo>() {
+ @Override
+ public int compare(Foo a, Foo b) {
+ return (a.x < b.x) ? -1 : ((a.x > b.x) ? 1 : 0);
+ }
+ };
+
+ @Test
+ public void iterableOrderedByBaseClassComparator() {
+ Iterable<Bar> targetList = asList(new Bar(1), new Bar(2), new Bar(3));
+ assertThat(targetList).isInOrder(FOO_COMPARATOR);
+ assertThat(targetList).isInStrictOrder(FOO_COMPARATOR);
+ }
+
+ @Test
+ public void isIn() {
+ ImmutableList<String> actual = ImmutableList.of("a");
+ ImmutableList<String> expectedA = ImmutableList.of("a");
+ ImmutableList<String> expectedB = ImmutableList.of("b");
+ ImmutableList<ImmutableList<String>> expected = ImmutableList.of(expectedA, expectedB);
+
+ assertThat(actual).isIn(expected);
+ }
+
+ @Test
+ public void isNotIn() {
+ ImmutableList<String> actual = ImmutableList.of("a");
+
+ assertThat(actual).isNotIn(ImmutableList.of(ImmutableList.of("b"), ImmutableList.of("c")));
+
+ expectFailureWhenTestingThat(actual).isNotIn(ImmutableList.of("a", "b"));
+ assertThat(expectFailure.getFailure())
+ .hasMessageThat()
+ .isEqualTo(
+ "The actual value is an Iterable, and you've written a test that compares it to some "
+ + "objects that are not Iterables. Did you instead mean to check whether its "
+ + "*contents* match any of the *contents* of the given values? If so, call "
+ + "containsNoneOf(...)/containsNoneIn(...) instead. Non-iterables: [a, b]");
+ }
+
+ @Test
+ public void isAnyOf() {
+ ImmutableList<String> actual = ImmutableList.of("a");
+ ImmutableList<String> expectedA = ImmutableList.of("a");
+ ImmutableList<String> expectedB = ImmutableList.of("b");
+
+ assertThat(actual).isAnyOf(expectedA, expectedB);
+ }
+
+ @Test
+ @SuppressWarnings("IncompatibleArgumentType")
+ public void isNoneOf() {
+ ImmutableList<String> actual = ImmutableList.of("a");
+
+ assertThat(actual).isNoneOf(ImmutableList.of("b"), ImmutableList.of("c"));
+
+ expectFailureWhenTestingThat(actual).isNoneOf("a", "b");
+ assertThat(expectFailure.getFailure())
+ .hasMessageThat()
+ .isEqualTo(
+ "The actual value is an Iterable, and you've written a test that compares it to some "
+ + "objects that are not Iterables. Did you instead mean to check whether its "
+ + "*contents* match any of the *contents* of the given values? If so, call "
+ + "containsNoneOf(...)/containsNoneIn(...) instead. Non-iterables: [a, b]");
+ }
+
+ private static final class CountsToStringCalls {
+ int calls;
+
+ @Override
+ public String toString() {
+ calls++;
+ return super.toString();
+ }
+ }
+
+ private IterableSubject expectFailureWhenTestingThat(Iterable<?> actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/LongSubjectTest.java b/core/src/test/java/com/google/common/truth/LongSubjectTest.java
new file mode 100644
index 00000000..b3a3f443
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/LongSubjectTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Long Subjects.
+ *
+ * @author David Saff
+ * @author Christian Gruber
+ * @author Kurt Alfred Kluever
+ */
+@RunWith(JUnit4.class)
+public class LongSubjectTest extends BaseSubjectTestCase {
+
+ @Test
+ public void simpleEquality() {
+ assertThat(4L).isEqualTo(4L);
+ }
+
+ @Test
+ public void simpleInequality() {
+ assertThat(4L).isNotEqualTo(5L);
+ }
+
+ @Test
+ public void equalityWithInts() {
+ assertThat(0L).isEqualTo(0);
+ expectFailureWhenTestingThat(0L).isNotEqualTo(0);
+ }
+
+ @Test
+ public void equalityFail() {
+ expectFailureWhenTestingThat(4L).isEqualTo(5L);
+ }
+
+ @Test
+ public void inequalityFail() {
+ expectFailureWhenTestingThat(4L).isNotEqualTo(4L);
+ }
+
+ @Test
+ public void equalityOfNulls() {
+ assertThat((Long) null).isEqualTo(null);
+ }
+
+ @Test
+ public void equalityOfNullsFail_nullActual() {
+ expectFailureWhenTestingThat(null).isEqualTo(5L);
+ }
+
+ @Test
+ public void equalityOfNullsFail_nullExpected() {
+ expectFailureWhenTestingThat(5L).isEqualTo(null);
+ }
+
+ @Test
+ public void inequalityOfNulls() {
+ assertThat(4L).isNotEqualTo(null);
+ assertThat((Integer) null).isNotEqualTo(4L);
+ }
+
+ @Test
+ public void inequalityOfNullsFail() {
+ expectFailureWhenTestingThat(null).isNotEqualTo(null);
+ }
+
+ @Test
+ public void testNumericTypeWithSameValue_shouldBeEqual_long_long() {
+ expectFailureWhenTestingThat(42L).isNotEqualTo(42L);
+ }
+
+ @Test
+ public void testNumericTypeWithSameValue_shouldBeEqual_long_int() {
+ expectFailureWhenTestingThat(42L).isNotEqualTo(42);
+ }
+
+ @Test
+ public void isGreaterThan_int_strictly() {
+ expectFailureWhenTestingThat(2L).isGreaterThan(3);
+ }
+
+ @Test
+ public void isGreaterThan_int() {
+ expectFailureWhenTestingThat(2L).isGreaterThan(2);
+ assertThat(2L).isGreaterThan(1);
+ }
+
+ @Test
+ public void isLessThan_int_strictly() {
+ expectFailureWhenTestingThat(2L).isLessThan(1);
+ }
+
+ @Test
+ public void isLessThan_int() {
+ expectFailureWhenTestingThat(2L).isLessThan(2);
+ assertThat(2L).isLessThan(3);
+ }
+
+ @Test
+ public void isAtLeast_int() {
+ expectFailureWhenTestingThat(2L).isAtLeast(3);
+ assertThat(2L).isAtLeast(2);
+ assertThat(2L).isAtLeast(1);
+ }
+
+ @Test
+ public void isAtMost_int() {
+ expectFailureWhenTestingThat(2L).isAtMost(1);
+ assertThat(2L).isAtMost(2);
+ assertThat(2L).isAtMost(3);
+ }
+
+ private LongSubject expectFailureWhenTestingThat(Long actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/MapSubjectTest.java b/core/src/test/java/com/google/common/truth/MapSubjectTest.java
new file mode 100644
index 00000000..6e28fdb8
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/MapSubjectTest.java
@@ -0,0 +1,2167 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.TestCorrespondences.CASE_INSENSITIVE_EQUALITY;
+import static com.google.common.truth.TestCorrespondences.INT_DIFF_FORMATTER;
+import static com.google.common.truth.TestCorrespondences.STRING_PARSES_TO_INTEGER_CORRESPONDENCE;
+import static com.google.common.truth.TestCorrespondences.WITHIN_10_OF;
+import static com.google.common.truth.Truth.assertThat;
+import static java.lang.String.CASE_INSENSITIVE_ORDER;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.TreeMap;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link Map} subjects.
+ *
+ * @author Christian Gruber
+ * @author Kurt Alfred Kluever
+ */
+@RunWith(JUnit4.class)
+public class MapSubjectTest extends BaseSubjectTestCase {
+
+ @Test
+ public void containsExactlyWithNullKey() {
+ Map<String, String> actual = Maps.newHashMap();
+ actual.put(null, "value");
+
+ assertThat(actual).containsExactly(null, "value");
+ assertThat(actual).containsExactly(null, "value").inOrder();
+ assertThat(actual).containsExactlyEntriesIn(actual);
+ assertThat(actual).containsExactlyEntriesIn(actual).inOrder();
+ }
+
+ @Test
+ public void containsExactlyWithNullValue() {
+ Map<String, String> actual = Maps.newHashMap();
+ actual.put("key", null);
+
+ assertThat(actual).containsExactly("key", null);
+ assertThat(actual).containsExactly("key", null).inOrder();
+ assertThat(actual).containsExactlyEntriesIn(actual);
+ assertThat(actual).containsExactlyEntriesIn(actual).inOrder();
+ }
+
+ @Test
+ public void containsExactlyEmpty() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of();
+
+ assertThat(actual).containsExactly();
+ assertThat(actual).containsExactly().inOrder();
+ assertThat(actual).containsExactlyEntriesIn(actual);
+ assertThat(actual).containsExactlyEntriesIn(actual).inOrder();
+ }
+
+ @Test
+ public void containsExactlyEmpty_fails() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1);
+
+ expectFailureWhenTestingThat(actual).containsExactly();
+ assertFailureKeys("expected to be empty", "but was");
+ }
+
+ @Test
+ public void containsExactlyEntriesInEmpty_fails() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1);
+
+ expectFailureWhenTestingThat(actual).containsExactlyEntriesIn(ImmutableMap.of());
+ assertFailureKeys("expected to be empty", "but was");
+ }
+
+ @Test
+ public void containsExactlyOneEntry() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1);
+
+ assertThat(actual).containsExactly("jan", 1);
+ assertThat(actual).containsExactly("jan", 1).inOrder();
+ assertThat(actual).containsExactlyEntriesIn(actual);
+ assertThat(actual).containsExactlyEntriesIn(actual).inOrder();
+ }
+
+ @Test
+ public void containsExactlyMultipleEntries() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+
+ assertThat(actual).containsExactly("march", 3, "jan", 1, "feb", 2);
+ assertThat(actual).containsExactly("jan", 1, "feb", 2, "march", 3).inOrder();
+ assertThat(actual).containsExactlyEntriesIn(actual);
+ assertThat(actual).containsExactlyEntriesIn(actual).inOrder();
+ }
+
+ @Test
+ public void containsExactlyDuplicateKeys() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+
+ try {
+ assertThat(actual).containsExactly("jan", 1, "jan", 2, "jan", 3);
+ fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException expected) {
+ assertThat(expected)
+ .hasMessageThat()
+ .isEqualTo("Duplicate keys ([jan x 3]) cannot be passed to containsExactly().");
+ }
+ }
+
+ @Test
+ public void containsExactlyMultipleDuplicateKeys() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+
+ try {
+ assertThat(actual).containsExactly("jan", 1, "jan", 1, "feb", 2, "feb", 2);
+ fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException expected) {
+ assertThat(expected)
+ .hasMessageThat()
+ .isEqualTo("Duplicate keys ([jan x 2, feb x 2]) cannot be passed to containsExactly().");
+ }
+ }
+
+ @Test
+ public void containsExactlyExtraKey() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+ expectFailureWhenTestingThat(actual).containsExactly("feb", 2, "jan", 1);
+ assertFailureKeys(
+ "unexpected keys", "for key", "unexpected value", "---", "expected", "but was");
+ assertFailureValue("for key", "march");
+ assertFailureValue("unexpected value", "3");
+ assertFailureValue("expected", "{feb=2, jan=1}");
+ assertFailureValue("but was", "{jan=1, feb=2, march=3}");
+ }
+
+ @Test
+ public void containsExactlyExtraKeyInOrder() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+ expectFailureWhenTestingThat(actual).containsExactly("feb", 2, "jan", 1).inOrder();
+ assertFailureKeys(
+ "unexpected keys", "for key", "unexpected value", "---", "expected", "but was");
+ assertFailureValue("for key", "march");
+ assertFailureValue("unexpected value", "3");
+ }
+
+ @Test
+ public void containsExactlyMissingKey() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2);
+ expectFailureWhenTestingThat(actual).containsExactly("jan", 1, "march", 3, "feb", 2);
+ assertFailureKeys("missing keys", "for key", "expected value", "---", "expected", "but was");
+ assertFailureValue("for key", "march");
+ assertFailureValue("expected value", "3");
+ }
+
+ @Test
+ public void containsExactlyWrongValue() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+ expectFailureWhenTestingThat(actual).containsExactly("jan", 1, "march", 33, "feb", 2);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected",
+ "but was");
+ assertFailureValue("for key", "march");
+ assertFailureValue("expected value", "33");
+ assertFailureValue("but got value", "3");
+ }
+
+ @Test
+ public void containsExactlyWrongValueWithNull() {
+ // Test for https://github.com/google/truth/issues/468
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+ expectFailureWhenTestingThat(actual).containsExactly("jan", 1, "march", null, "feb", 2);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected",
+ "but was");
+ assertFailureValue("for key", "march");
+ assertFailureValue("expected value", "null");
+ assertFailureValue("but got value", "3");
+ }
+
+ @Test
+ public void containsExactlyExtraKeyAndMissingKey() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "march", 3);
+ expectFailureWhenTestingThat(actual).containsExactly("jan", 1, "feb", 2);
+ assertFailureKeys(
+ "missing keys",
+ "for key",
+ "expected value",
+ "unexpected keys",
+ "for key",
+ "unexpected value",
+ "---",
+ "expected",
+ "but was");
+ assertFailureValueIndexed("for key", 0, "feb");
+ assertFailureValue("expected value", "2");
+ assertFailureValueIndexed("for key", 1, "march");
+ assertFailureValue("unexpected value", "3");
+ }
+
+ @Test
+ public void containsExactlyExtraKeyAndWrongValue() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+ expectFailureWhenTestingThat(actual).containsExactly("jan", 1, "march", 33);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "unexpected keys",
+ "for key",
+ "unexpected value",
+ "---",
+ "expected",
+ "but was");
+ assertFailureValueIndexed("for key", 0, "march");
+ assertFailureValue("expected value", "33");
+ assertFailureValue("but got value", "3");
+ assertFailureValueIndexed("for key", 1, "feb");
+ assertFailureValue("unexpected value", "2");
+ }
+
+ @Test
+ public void containsExactlyMissingKeyAndWrongValue() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "march", 3);
+ expectFailureWhenTestingThat(actual).containsExactly("jan", 1, "march", 33, "feb", 2);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "missing keys",
+ "for key",
+ "expected value",
+ "---",
+ "expected",
+ "but was");
+ assertFailureValueIndexed("for key", 0, "march");
+ assertFailureValueIndexed("expected value", 0, "33");
+ assertFailureValue("but got value", "3");
+ assertFailureValueIndexed("for key", 1, "feb");
+ assertFailureValueIndexed("expected value", 1, "2");
+ }
+
+ @Test
+ public void containsExactlyExtraKeyAndMissingKeyAndWrongValue() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "march", 3);
+ expectFailureWhenTestingThat(actual).containsExactly("march", 33, "feb", 2);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "missing keys",
+ "for key",
+ "expected value",
+ "unexpected keys",
+ "for key",
+ "unexpected value",
+ "---",
+ "expected",
+ "but was");
+ assertFailureValueIndexed("for key", 0, "march");
+ assertFailureValueIndexed("expected value", 0, "33");
+ assertFailureValue("but got value", "3");
+ assertFailureValueIndexed("for key", 1, "feb");
+ assertFailureValueIndexed("expected value", 1, "2");
+ assertFailureValueIndexed("for key", 2, "jan");
+ assertFailureValue("unexpected value", "1");
+ }
+
+ @Test
+ public void containsExactlyNotInOrder() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+ assertThat(actual).containsExactlyEntriesIn(actual);
+ assertThat(actual).containsExactlyEntriesIn(actual).inOrder();
+
+ assertThat(actual).containsExactly("jan", 1, "march", 3, "feb", 2);
+ expectFailureWhenTestingThat(actual).containsExactly("jan", 1, "march", 3, "feb", 2).inOrder();
+ assertFailureKeys("entries match, but order was wrong", "expected", "but was");
+ assertFailureValue("expected", "{jan=1, march=3, feb=2}");
+ assertFailureValue("but was", "{jan=1, feb=2, march=3}");
+ }
+
+ @Test
+ @SuppressWarnings("ShouldHaveEvenArgs")
+ public void containsExactlyBadNumberOfArgs() {
+ ImmutableMap<String, Integer> actual =
+ ImmutableMap.of("jan", 1, "feb", 2, "march", 3, "april", 4, "may", 5);
+ assertThat(actual).containsExactlyEntriesIn(actual);
+ assertThat(actual).containsExactlyEntriesIn(actual).inOrder();
+
+ try {
+ assertThat(actual)
+ .containsExactly("jan", 1, "feb", 2, "march", 3, "april", 4, "may", 5, "june", 6, "july");
+ fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException expected) {
+ assertThat(expected)
+ .hasMessageThat()
+ .isEqualTo(
+ "There must be an equal number of key/value pairs "
+ + "(i.e., the number of key/value parameters (13) must be even).");
+ }
+ }
+
+ @Test
+ public void containsExactlyWrongValue_sameToStringForValues() {
+ expectFailureWhenTestingThat(ImmutableMap.of("jan", 1L, "feb", 2L))
+ .containsExactly("jan", 1, "feb", 2);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected",
+ "but was");
+ assertFailureValueIndexed("for key", 0, "jan");
+ assertFailureValueIndexed("expected value", 0, "1 (java.lang.Integer)");
+ assertFailureValueIndexed("but got value", 0, "1 (java.lang.Long)");
+ assertFailureValueIndexed("for key", 1, "feb");
+ assertFailureValueIndexed("expected value", 1, "2 (java.lang.Integer)");
+ assertFailureValueIndexed("but got value", 1, "2 (java.lang.Long)");
+ }
+
+ @Test
+ public void containsExactlyWrongValue_sameToStringForKeys() {
+ expectFailureWhenTestingThat(ImmutableMap.of(1L, "jan", 1, "feb"))
+ .containsExactly(1, "jan", 1L, "feb");
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected",
+ "but was");
+ assertFailureValueIndexed("for key", 0, "1 (java.lang.Integer)");
+ assertFailureValueIndexed("expected value", 0, "jan");
+ assertFailureValueIndexed("but got value", 0, "feb");
+ assertFailureValueIndexed("for key", 1, "1 (java.lang.Long)");
+ assertFailureValueIndexed("expected value", 1, "feb");
+ assertFailureValueIndexed("but got value", 1, "jan");
+ }
+
+ @Test
+ public void containsExactlyExtraKeyAndMissingKey_failsWithSameToStringForKeys() {
+ expectFailureWhenTestingThat(ImmutableMap.of(1L, "jan", 2, "feb"))
+ .containsExactly(1, "jan", 2, "feb");
+ assertFailureKeys(
+ "missing keys",
+ "for key",
+ "expected value",
+ "unexpected keys",
+ "for key",
+ "unexpected value",
+ "---",
+ "expected",
+ "but was");
+ assertFailureValueIndexed("for key", 0, "1 (java.lang.Integer)");
+ assertFailureValue("expected value", "jan");
+ assertFailureValueIndexed("for key", 1, "1 (java.lang.Long)");
+ assertFailureValue("unexpected value", "jan");
+ }
+
+ @Test
+ public void containsAtLeastWithNullKey() {
+ Map<String, String> actual = Maps.newHashMap();
+ actual.put(null, "value");
+ actual.put("unexpectedKey", "unexpectedValue");
+ Map<String, String> expected = Maps.newHashMap();
+ expected.put(null, "value");
+
+ assertThat(actual).containsAtLeast(null, "value");
+ assertThat(actual).containsAtLeast(null, "value").inOrder();
+ assertThat(actual).containsAtLeastEntriesIn(expected);
+ assertThat(actual).containsAtLeastEntriesIn(expected).inOrder();
+ }
+
+ @Test
+ public void containsAtLeastWithNullValue() {
+ Map<String, String> actual = Maps.newHashMap();
+ actual.put("key", null);
+ actual.put("unexpectedKey", "unexpectedValue");
+ Map<String, String> expected = Maps.newHashMap();
+ expected.put("key", null);
+
+ assertThat(actual).containsAtLeast("key", null);
+ assertThat(actual).containsAtLeast("key", null).inOrder();
+ assertThat(actual).containsAtLeastEntriesIn(expected);
+ assertThat(actual).containsAtLeastEntriesIn(expected).inOrder();
+ }
+
+ @Test
+ public void containsAtLeastEmpty() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("key", 1);
+
+ assertThat(actual).containsAtLeastEntriesIn(ImmutableMap.of());
+ assertThat(actual).containsAtLeastEntriesIn(ImmutableMap.of()).inOrder();
+ }
+
+ @Test
+ public void containsAtLeastOneEntry() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1);
+
+ assertThat(actual).containsAtLeast("jan", 1);
+ assertThat(actual).containsAtLeast("jan", 1).inOrder();
+ assertThat(actual).containsAtLeastEntriesIn(actual);
+ assertThat(actual).containsAtLeastEntriesIn(actual).inOrder();
+ }
+
+ @Test
+ public void containsAtLeastMultipleEntries() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2, "mar", 3, "apr", 4);
+
+ assertThat(actual).containsAtLeast("apr", 4, "jan", 1, "feb", 2);
+ assertThat(actual).containsAtLeast("jan", 1, "feb", 2, "apr", 4).inOrder();
+ assertThat(actual).containsAtLeastEntriesIn(ImmutableMap.of("apr", 4, "jan", 1, "feb", 2));
+ assertThat(actual).containsAtLeastEntriesIn(actual).inOrder();
+ }
+
+ @Test
+ public void containsAtLeastDuplicateKeys() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+
+ try {
+ assertThat(actual).containsAtLeast("jan", 1, "jan", 2, "jan", 3);
+ fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException expected) {
+ assertThat(expected)
+ .hasMessageThat()
+ .isEqualTo("Duplicate keys ([jan x 3]) cannot be passed to containsAtLeast().");
+ }
+ }
+
+ @Test
+ public void containsAtLeastMultipleDuplicateKeys() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+
+ try {
+ assertThat(actual).containsAtLeast("jan", 1, "jan", 1, "feb", 2, "feb", 2);
+ fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException expected) {
+ assertThat(expected)
+ .hasMessageThat()
+ .isEqualTo("Duplicate keys ([jan x 2, feb x 2]) cannot be passed to containsAtLeast().");
+ }
+ }
+
+ @Test
+ public void containsAtLeastMissingKey() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2);
+ expectFailureWhenTestingThat(actual).containsAtLeast("jan", 1, "march", 3);
+ assertFailureKeys(
+ "missing keys",
+ "for key",
+ "expected value",
+ "---",
+ "expected to contain at least",
+ "but was");
+ assertFailureValue("for key", "march");
+ assertFailureValue("expected value", "3");
+ assertFailureValue("expected to contain at least", "{jan=1, march=3}");
+ }
+
+ @Test
+ public void containsAtLeastWrongValue() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+ expectFailureWhenTestingThat(actual).containsAtLeast("jan", 1, "march", 33);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected to contain at least",
+ "but was");
+ assertFailureValue("for key", "march");
+ assertFailureValue("expected value", "33");
+ assertFailureValue("but got value", "3");
+ }
+
+ @Test
+ public void containsAtLeastWrongValueWithNull() {
+ // Test for https://github.com/google/truth/issues/468
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+ expectFailureWhenTestingThat(actual).containsAtLeast("jan", 1, "march", null);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected to contain at least",
+ "but was");
+ assertFailureValue("for key", "march");
+ assertFailureValue("expected value", "null");
+ assertFailureValue("but got value", "3");
+ }
+
+ @Test
+ public void containsAtLeastExtraKeyAndMissingKeyAndWrongValue() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "march", 3);
+ expectFailureWhenTestingThat(actual).containsAtLeast("march", 33, "feb", 2);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "missing keys",
+ "for key",
+ "expected value",
+ "---",
+ "expected to contain at least",
+ "but was");
+ assertFailureValueIndexed("for key", 0, "march");
+ assertFailureValueIndexed("expected value", 0, "33");
+ assertFailureValue("but got value", "3");
+ assertFailureValueIndexed("for key", 1, "feb");
+ assertFailureValueIndexed("expected value", 1, "2");
+ }
+
+ @Test
+ public void containsAtLeastNotInOrder() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+
+ assertThat(actual).containsAtLeast("march", 3, "feb", 2);
+ expectFailureWhenTestingThat(actual).containsAtLeast("march", 3, "feb", 2).inOrder();
+ assertFailureKeys(
+ "required entries were all found, but order was wrong",
+ "expected to contain at least",
+ "but was");
+ assertFailureValue("expected to contain at least", "{march=3, feb=2}");
+ assertFailureValue("but was", "{jan=1, feb=2, march=3}");
+ }
+
+ @Test
+ @SuppressWarnings("ShouldHaveEvenArgs")
+ public void containsAtLeastBadNumberOfArgs() {
+ ImmutableMap<String, Integer> actual =
+ ImmutableMap.of("jan", 1, "feb", 2, "march", 3, "april", 4, "may", 5);
+
+ try {
+ assertThat(actual)
+ .containsAtLeast("jan", 1, "feb", 2, "march", 3, "april", 4, "may", 5, "june", 6, "july");
+ fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException expected) {
+ assertThat(expected)
+ .hasMessageThat()
+ .isEqualTo(
+ "There must be an equal number of key/value pairs "
+ + "(i.e., the number of key/value parameters (13) must be even).");
+ }
+ }
+
+ @Test
+ public void containsAtLeastWrongValue_sameToStringForValues() {
+ expectFailureWhenTestingThat(ImmutableMap.of("jan", 1L, "feb", 2L, "mar", 3L))
+ .containsAtLeast("jan", 1, "feb", 2);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected to contain at least",
+ "but was");
+ assertFailureValueIndexed("for key", 0, "jan");
+ assertFailureValueIndexed("expected value", 0, "1 (java.lang.Integer)");
+ assertFailureValueIndexed("but got value", 0, "1 (java.lang.Long)");
+ assertFailureValueIndexed("for key", 1, "feb");
+ assertFailureValueIndexed("expected value", 1, "2 (java.lang.Integer)");
+ assertFailureValueIndexed("but got value", 1, "2 (java.lang.Long)");
+ }
+
+ @Test
+ public void containsAtLeastWrongValue_sameToStringForKeys() {
+ expectFailureWhenTestingThat(ImmutableMap.of(1L, "jan", 1, "feb"))
+ .containsAtLeast(1, "jan", 1L, "feb");
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected to contain at least",
+ "but was");
+ assertFailureValueIndexed("for key", 0, "1 (java.lang.Integer)");
+ assertFailureValueIndexed("expected value", 0, "jan");
+ assertFailureValueIndexed("but got value", 0, "feb");
+ assertFailureValueIndexed("for key", 1, "1 (java.lang.Long)");
+ assertFailureValueIndexed("expected value", 1, "feb");
+ assertFailureValueIndexed("but got value", 1, "jan");
+ }
+
+ @Test
+ public void containsAtLeastExtraKeyAndMissingKey_failsWithSameToStringForKeys() {
+ expectFailureWhenTestingThat(ImmutableMap.of(1L, "jan", 2, "feb"))
+ .containsAtLeast(1, "jan", 2, "feb");
+ assertFailureKeys(
+ "missing keys",
+ "for key",
+ "expected value",
+ "---",
+ "expected to contain at least",
+ "but was");
+ assertFailureValue("for key", "1 (java.lang.Integer)");
+ assertFailureValue("expected value", "jan");
+ }
+
+ @Test
+ public void isEqualToPass() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+ ImmutableMap<String, Integer> expectedMap = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+
+ assertThat(actual).isEqualTo(expectedMap);
+ }
+
+ @Test
+ public void isEqualToFailureExtraMissingAndDiffering() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+ ImmutableMap<String, Integer> expectedMap = ImmutableMap.of("jan", 1, "april", 4, "march", 5);
+
+ expectFailureWhenTestingThat(actual).isEqualTo(expectedMap);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "missing keys",
+ "for key",
+ "expected value",
+ "unexpected keys",
+ "for key",
+ "unexpected value",
+ "---",
+ "expected",
+ "but was");
+ assertFailureValueIndexed("for key", 0, "march");
+ assertFailureValueIndexed("expected value", 0, "5");
+ assertFailureValue("but got value", "3");
+ assertFailureValueIndexed("for key", 1, "april");
+ assertFailureValueIndexed("expected value", 1, "4");
+ assertFailureValueIndexed("for key", 2, "feb");
+ assertFailureValue("unexpected value", "2");
+ assertFailureValue("expected", "{jan=1, april=4, march=5}");
+ assertFailureValue("but was", "{jan=1, feb=2, march=3}");
+ }
+
+ @Test
+ public void isEqualToFailureDiffering() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+ ImmutableMap<String, Integer> expectedMap = ImmutableMap.of("jan", 1, "feb", 2, "march", 4);
+
+ expectFailureWhenTestingThat(actual).isEqualTo(expectedMap);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected",
+ "but was");
+ assertFailureValueIndexed("for key", 0, "march");
+ assertFailureValue("expected value", "4");
+ assertFailureValue("but got value", "3");
+ }
+
+ @Test
+ public void isEqualToFailureExtra() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+ ImmutableMap<String, Integer> expectedMap = ImmutableMap.of("jan", 1, "feb", 2);
+
+ expectFailureWhenTestingThat(actual).isEqualTo(expectedMap);
+ assertFailureKeys(
+ "unexpected keys", "for key", "unexpected value", "---", "expected", "but was");
+ assertFailureValue("for key", "march");
+ assertFailureValue("unexpected value", "3");
+ }
+
+ @Test
+ public void isEqualToFailureMissing() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2);
+ ImmutableMap<String, Integer> expectedMap = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+
+ expectFailureWhenTestingThat(actual).isEqualTo(expectedMap);
+ assertFailureKeys("missing keys", "for key", "expected value", "---", "expected", "but was");
+ assertFailureValue("for key", "march");
+ assertFailureValue("expected value", "3");
+ }
+
+ @Test
+ public void isEqualToFailureExtraAndMissing() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+ ImmutableMap<String, Integer> expectedMap = ImmutableMap.of("jan", 1, "feb", 2, "mar", 3);
+
+ expectFailureWhenTestingThat(actual).isEqualTo(expectedMap);
+ assertFailureKeys(
+ "missing keys",
+ "for key",
+ "expected value",
+ "unexpected keys",
+ "for key",
+ "unexpected value",
+ "---",
+ "expected",
+ "but was");
+ assertFailureValueIndexed("for key", 0, "mar");
+ assertFailureValue("expected value", "3");
+ assertFailureValueIndexed("for key", 1, "march");
+ assertFailureValue("unexpected value", "3");
+ }
+
+ @Test
+ public void isEqualToFailureDiffering_sameToString() {
+ ImmutableMap<String, Number> actual =
+ ImmutableMap.<String, Number>of("jan", 1, "feb", 2, "march", 3L);
+ ImmutableMap<String, Integer> expectedMap = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+
+ expectFailureWhenTestingThat(actual).isEqualTo(expectedMap);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected",
+ "but was");
+ assertFailureValueIndexed("for key", 0, "march");
+ assertFailureValue("expected value", "3 (java.lang.Integer)");
+ assertFailureValue("but got value", "3 (java.lang.Long)");
+ }
+
+ @Test
+ public void isEqualToNonMap() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+ expectFailureWhenTestingThat(actual).isEqualTo("something else");
+ assertFailureKeys("expected", "but was");
+ }
+
+ @Test
+ public void isEqualToNotConsistentWithEquals() {
+ TreeMap<String, Integer> actual = new TreeMap<>(CASE_INSENSITIVE_ORDER);
+ TreeMap<String, Integer> expected = new TreeMap<>(CASE_INSENSITIVE_ORDER);
+ actual.put("one", 1);
+ expected.put("ONE", 1);
+ /*
+ * Our contract doesn't guarantee that the following test will pass. It *currently* does,
+ * though, and if we change that behavior, we want this test to let us know.
+ */
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ public void isEqualToNotConsistentWithEquals_failure() {
+ TreeMap<String, Integer> actual = new TreeMap<>(CASE_INSENSITIVE_ORDER);
+ TreeMap<String, Integer> expected = new TreeMap<>(CASE_INSENSITIVE_ORDER);
+ actual.put("one", 1);
+ expected.put("ONE", 1);
+ actual.put("two", 2);
+ expectFailureWhenTestingThat(actual).isEqualTo(expected);
+ // The exact message generated is unspecified.
+ }
+
+ @Test
+ public void isEqualToActualNullOtherMap() {
+ expectFailureWhenTestingThat(null).isEqualTo(ImmutableMap.of());
+ }
+
+ @Test
+ public void isEqualToActualMapOtherNull() {
+ expectFailureWhenTestingThat(ImmutableMap.of()).isEqualTo(null);
+ }
+
+ @Test
+ public void isNotEqualTo() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+ ImmutableMap<String, Integer> unexpected = ImmutableMap.of("jan", 1, "feb", 2, "march", 3);
+
+ expectFailureWhenTestingThat(actual).isNotEqualTo(unexpected);
+ }
+
+ @Test
+ public void isEmpty() {
+ ImmutableMap<String, String> actual = ImmutableMap.of();
+ assertThat(actual).isEmpty();
+ }
+
+ @Test
+ public void isEmptyWithFailure() {
+ ImmutableMap<Integer, Integer> actual = ImmutableMap.of(1, 5);
+ expectFailureWhenTestingThat(actual).isEmpty();
+ assertFailureKeys("expected to be empty", "but was");
+ }
+
+ @Test
+ public void isNotEmpty() {
+ ImmutableMap<Integer, Integer> actual = ImmutableMap.of(1, 5);
+ assertThat(actual).isNotEmpty();
+ }
+
+ @Test
+ public void isNotEmptyWithFailure() {
+ ImmutableMap<Integer, Integer> actual = ImmutableMap.of();
+ expectFailureWhenTestingThat(actual).isNotEmpty();
+ assertFailureKeys("expected not to be empty");
+ }
+
+ @Test
+ public void hasSize() {
+ assertThat(ImmutableMap.of(1, 2, 3, 4)).hasSize(2);
+ }
+
+ @Test
+ public void hasSizeZero() {
+ assertThat(ImmutableMap.of()).hasSize(0);
+ }
+
+ @Test
+ public void hasSizeNegative() {
+ try {
+ assertThat(ImmutableMap.of(1, 2)).hasSize(-1);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test
+ public void containsKey() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("kurt", "kluever");
+ assertThat(actual).containsKey("kurt");
+ }
+
+ @Test
+ public void containsKeyFailure() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("kurt", "kluever");
+ expectFailureWhenTestingThat(actual).containsKey("greg");
+ assertFailureKeys("value of", "expected to contain", "but was", "map was");
+ assertFailureValue("value of", "map.keySet()");
+ assertFailureValue("expected to contain", "greg");
+ assertFailureValue("but was", "[kurt]");
+ }
+
+ @Test
+ public void containsKeyNullFailure() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("kurt", "kluever");
+ expectFailureWhenTestingThat(actual).containsKey(null);
+ assertFailureKeys("value of", "expected to contain", "but was", "map was");
+ assertFailureValue("value of", "map.keySet()");
+ assertFailureValue("expected to contain", "null");
+ assertFailureValue("but was", "[kurt]");
+ }
+
+ @Test
+ public void containsKey_failsWithSameToString() {
+ expectFailureWhenTestingThat(ImmutableMap.of(1L, "value1", 2L, "value2", "1", "value3"))
+ .containsKey(1);
+ assertFailureKeys(
+ "value of",
+ "expected to contain",
+ "an instance of",
+ "but did not",
+ "though it did contain",
+ "full contents",
+ "map was");
+ assertFailureValue("value of", "map.keySet()");
+ assertFailureValue("expected to contain", "1");
+ }
+
+ @Test
+ public void containsKey_failsWithNullStringAndNull() {
+ Map<String, String> actual = Maps.newHashMap();
+ actual.put("null", "value1");
+
+ expectFailureWhenTestingThat(actual).containsKey(null);
+ assertFailureKeys(
+ "value of",
+ "expected to contain",
+ "an instance of",
+ "but did not",
+ "though it did contain",
+ "full contents",
+ "map was");
+ assertFailureValue("value of", "map.keySet()");
+ assertFailureValue("expected to contain", "null");
+ }
+
+ @Test
+ public void containsNullKey() {
+ Map<String, String> actual = Maps.newHashMap();
+ actual.put(null, "null");
+ assertThat(actual).containsKey(null);
+ }
+
+ @Test
+ public void doesNotContainKey() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("kurt", "kluever");
+ assertThat(actual).doesNotContainKey("greg");
+ assertThat(actual).doesNotContainKey(null);
+ }
+
+ @Test
+ public void doesNotContainKeyFailure() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("kurt", "kluever");
+ expectFailureWhenTestingThat(actual).doesNotContainKey("kurt");
+ assertFailureKeys("value of", "expected not to contain", "but was", "map was");
+ assertFailureValue("value of", "map.keySet()");
+ assertFailureValue("expected not to contain", "kurt");
+ assertFailureValue("but was", "[kurt]");
+ }
+
+ @Test
+ public void doesNotContainNullKey() {
+ Map<String, String> actual = Maps.newHashMap();
+ actual.put(null, "null");
+ expectFailureWhenTestingThat(actual).doesNotContainKey(null);
+ assertFailureKeys("value of", "expected not to contain", "but was", "map was");
+ assertFailureValue("value of", "map.keySet()");
+ assertFailureValue("expected not to contain", "null");
+ assertFailureValue("but was", "[null]");
+ }
+
+ @Test
+ public void containsEntry() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("kurt", "kluever");
+ assertThat(actual).containsEntry("kurt", "kluever");
+ }
+
+ @Test
+ public void containsEntryFailure() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("kurt", "kluever");
+ expectFailureWhenTestingThat(actual).containsEntry("greg", "kick");
+ assertFailureKeys("expected to contain entry", "but was");
+ assertFailureValue("expected to contain entry", "greg=kick");
+ assertFailureValue("but was", "{kurt=kluever}");
+ }
+
+ @Test
+ public void containsEntry_failsWithSameToStringOfKey() {
+ expectFailureWhenTestingThat(ImmutableMap.of(1L, "value1", 2L, "value2"))
+ .containsEntry(1, "value1");
+ assertFailureKeys(
+ "expected to contain entry",
+ "an instance of",
+ "but did not",
+ "though it did contain keys",
+ "full contents");
+ assertFailureValue("an instance of", "Map.Entry<java.lang.Integer, java.lang.String>");
+ assertFailureValue("though it did contain keys", "[1] (java.lang.Long)");
+ }
+
+ @Test
+ public void containsEntry_failsWithSameToStringOfValue() {
+ // Does not contain the correct key, but does contain a value which matches by toString.
+ expectFailureWhenTestingThat(ImmutableMap.of(1, "null")).containsEntry(2, null);
+ assertFailureKeys(
+ "expected to contain entry",
+ "an instance of",
+ "but did not",
+ "though it did contain values",
+ "full contents");
+ assertFailureValue("an instance of", "Map.Entry<java.lang.Integer, null type>");
+ assertFailureValue("though it did contain values", "[null] (java.lang.String)");
+ }
+
+ @Test
+ public void containsNullKeyAndValue() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("kurt", "kluever");
+ expectFailureWhenTestingThat(actual).containsEntry(null, null);
+ assertFailureKeys("expected to contain entry", "but was");
+ assertFailureValue("expected to contain entry", "null=null");
+ assertFailureValue("but was", "{kurt=kluever}");
+ }
+
+ @Test
+ public void containsNullEntry() {
+ Map<String, String> actual = Maps.newHashMap();
+ actual.put(null, null);
+ assertThat(actual).containsEntry(null, null);
+ }
+
+ @Test
+ public void containsNullEntryValue() {
+ Map<String, String> actual = Maps.newHashMap();
+ actual.put(null, null);
+ expectFailureWhenTestingThat(actual).containsEntry("kurt", null);
+ assertFailureKeys(
+ "expected to contain entry",
+ "but did not",
+ "though it did contain keys with that value",
+ "full contents");
+ assertFailureValue("expected to contain entry", "kurt=null");
+ assertFailureValue("though it did contain keys with that value", "[null]");
+ }
+
+ private static final String KEY_IS_PRESENT_WITH_DIFFERENT_VALUE =
+ "key is present but with a different value";
+
+ @Test
+ public void containsNullEntryKey() {
+ Map<String, String> actual = Maps.newHashMap();
+ actual.put(null, null);
+ expectFailureWhenTestingThat(actual).containsEntry(null, "kluever");
+ assertFailureValue("value of", "map.get(null)");
+ assertFailureValue("expected", "kluever");
+ assertFailureValue("but was", "null");
+ assertFailureValue("map was", "{null=null}");
+ assertThat(expectFailure.getFailure())
+ .hasMessageThat()
+ .contains(KEY_IS_PRESENT_WITH_DIFFERENT_VALUE);
+ }
+
+ @Test
+ public void containsExactly_bothExactAndToStringKeyMatches_showsExactKeyMatch() {
+ ImmutableMap<Number, String> actual = ImmutableMap.of(1, "actual int", 1L, "actual long");
+ expectFailureWhenTestingThat(actual).containsEntry(1L, "expected long");
+ // should show the exact key match, 1="actual int", not the toString key match, 1L="actual long"
+ assertFailureKeys("value of", "expected", "but was", "map was");
+ assertFailureValue("value of", "map.get(1)");
+ assertFailureValue("expected", "expected long");
+ assertFailureValue("but was", "actual long");
+ }
+
+ @Test
+ public void doesNotContainEntry() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("kurt", "kluever");
+ assertThat(actual).doesNotContainEntry("greg", "kick");
+ assertThat(actual).doesNotContainEntry(null, null);
+ assertThat(actual).doesNotContainEntry("kurt", null);
+ assertThat(actual).doesNotContainEntry(null, "kluever");
+ }
+
+ @Test
+ public void doesNotContainEntryFailure() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("kurt", "kluever");
+ expectFailureWhenTestingThat(actual).doesNotContainEntry("kurt", "kluever");
+ assertFailureKeys("value of", "expected not to contain", "but was");
+ assertFailureValue("value of", "map.entrySet()");
+ assertFailureValue("expected not to contain", "kurt=kluever");
+ assertFailureValue("but was", "[kurt=kluever]");
+ }
+
+ @Test
+ public void doesNotContainNullEntry() {
+ Map<String, String> actual = Maps.newHashMap();
+ actual.put(null, null);
+ assertThat(actual).doesNotContainEntry("kurt", null);
+ assertThat(actual).doesNotContainEntry(null, "kluever");
+ }
+
+ @Test
+ public void doesNotContainNullEntryFailure() {
+ Map<String, String> actual = Maps.newHashMap();
+ actual.put(null, null);
+ expectFailureWhenTestingThat(actual).doesNotContainEntry(null, null);
+ assertFailureKeys("value of", "expected not to contain", "but was");
+ assertFailureValue("value of", "map.entrySet()");
+ assertFailureValue("expected not to contain", "null=null");
+ assertFailureValue("but was", "[null=null]");
+ }
+
+ @Test
+ public void failMapContainsKey() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("a", "A");
+ expectFailureWhenTestingThat(actual).containsKey("b");
+ assertFailureKeys("value of", "expected to contain", "but was", "map was");
+ assertFailureValue("value of", "map.keySet()");
+ assertFailureValue("expected to contain", "b");
+ assertFailureValue("but was", "[a]");
+ }
+
+ @Test
+ public void failMapContainsKeyWithNull() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("a", "A");
+ expectFailureWhenTestingThat(actual).containsKey(null);
+ assertFailureKeys("value of", "expected to contain", "but was", "map was");
+ assertFailureValue("value of", "map.keySet()");
+ assertFailureValue("expected to contain", "null");
+ assertFailureValue("but was", "[a]");
+ }
+
+ @Test
+ public void failMapLacksKey() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("a", "A");
+ expectFailureWhenTestingThat(actual).doesNotContainKey("a");
+ assertFailureKeys("value of", "expected not to contain", "but was", "map was");
+ assertFailureValue("value of", "map.keySet()");
+ assertFailureValue("expected not to contain", "a");
+ assertFailureValue("but was", "[a]");
+ }
+
+ @Test
+ public void containsKeyWithValue() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("a", "A");
+ assertThat(actual).containsEntry("a", "A");
+ }
+
+ @Test
+ public void containsKeyWithNullValueNullExpected() {
+ Map<String, String> actual = Maps.newHashMap();
+ actual.put("a", null);
+ assertThat(actual).containsEntry("a", null);
+ }
+
+ @Test
+ public void failMapContainsKeyWithValue() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("a", "A");
+ expectFailureWhenTestingThat(actual).containsEntry("a", "a");
+ assertFailureValue("value of", "map.get(a)");
+ assertFailureValue("expected", "a");
+ assertFailureValue("but was", "A");
+ assertFailureValue("map was", "{a=A}");
+ assertThat(expectFailure.getFailure())
+ .hasMessageThat()
+ .doesNotContain(KEY_IS_PRESENT_WITH_DIFFERENT_VALUE);
+ }
+
+ @Test
+ public void failMapContainsKeyWithNullValuePresentExpected() {
+ Map<String, String> actual = Maps.newHashMap();
+ actual.put("a", null);
+ expectFailureWhenTestingThat(actual).containsEntry("a", "A");
+ assertFailureValue("value of", "map.get(a)");
+ assertFailureValue("expected", "A");
+ assertFailureValue("but was", "null");
+ assertFailureValue("map was", "{a=null}");
+ assertThat(expectFailure.getFailure())
+ .hasMessageThat()
+ .contains(KEY_IS_PRESENT_WITH_DIFFERENT_VALUE);
+ }
+
+ @Test
+ public void failMapContainsKeyWithPresentValueNullExpected() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("a", "A");
+ expectFailureWhenTestingThat(actual).containsEntry("a", null);
+ assertFailureValue("value of", "map.get(a)");
+ assertFailureValue("expected", "null");
+ assertFailureValue("but was", "A");
+ assertFailureValue("map was", "{a=A}");
+ assertThat(expectFailure.getFailure())
+ .hasMessageThat()
+ .contains(KEY_IS_PRESENT_WITH_DIFFERENT_VALUE);
+ }
+
+ @Test
+ public void comparingValuesUsing_containsEntry_success() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456");
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsEntry("def", 456);
+ }
+
+ @Test
+ public void comparingValuesUsing_containsEntry_failsExpectedKeyHasWrongValue() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "+123", "def", "+456");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsEntry("def", 123);
+ assertFailureKeys("for key", "expected value", "testing whether", "but got value", "full map");
+ assertFailureValue("for key", "def");
+ assertFailureValue("expected value", "123");
+ assertFailureValue("testing whether", "actual value parses to expected value");
+ assertFailureValue("but got value", "+456");
+ assertFailureValue("full map", "{abc=+123, def=+456}");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsEntry_failsWrongKeyHasExpectedValue() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "+123", "def", "+456");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsEntry("xyz", 456);
+ assertFailureKeys(
+ "for key",
+ "expected value",
+ "testing whether",
+ "but was missing",
+ "other keys with matching values",
+ "full map");
+ assertFailureValue("other keys with matching values", "[def]");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsEntry_failsMissingExpectedKeyAndValue() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "+123", "def", "+456");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsEntry("xyz", 321);
+ assertFailureKeys(
+ "for key", "expected value", "testing whether", "but was missing", "full map");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsEntry_diffExpectedKeyHasWrongValue() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("abc", 35, "def", 71);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(WITHIN_10_OF)
+ .containsEntry("def", 60);
+ assertFailureKeys(
+ "for key", "expected value", "testing whether", "but got value", "diff", "full map");
+ assertFailureValue("for key", "def");
+ assertFailureValue("expected value", "60");
+ assertFailureValue("but got value", "71");
+ assertFailureValue("diff", "11");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsEntry_handlesFormatDiffExceptions() {
+ Map<String, Integer> actual = new LinkedHashMap<>();
+ actual.put("abc", 35);
+ actual.put("def", null);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(WITHIN_10_OF)
+ .containsEntry("def", 60);
+ assertFailureKeys(
+ "for key",
+ "expected value",
+ "testing whether",
+ "but got value",
+ "full map",
+ "additionally, one or more exceptions were thrown while comparing values",
+ "first exception",
+ "additionally, one or more exceptions were thrown while formatting diffs",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception", 0)
+ .startsWith("compare(null, 60) threw java.lang.NullPointerException");
+ assertThatFailure()
+ .factValue("first exception", 1)
+ .startsWith("formatDiff(null, 60) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsEntry_handlesExceptions_expectedKeyHasWrongValue() {
+ Map<Integer, String> actual = new LinkedHashMap<>();
+ actual.put(1, "one");
+ actual.put(2, null);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(CASE_INSENSITIVE_EQUALITY)
+ .containsEntry(2, "TWO");
+ // The test fails because the expected key has a null value which causes compare() to throw.
+ // We should report that the key has the wrong value, and also that we saw an exception.
+ assertFailureKeys(
+ "for key",
+ "expected value",
+ "testing whether",
+ "but got value",
+ "full map",
+ "additionally, one or more exceptions were thrown while comparing values",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, TWO) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsEntry_handlesExceptions_wrongKeyHasExpectedValue() {
+ Map<Integer, String> actual = new LinkedHashMap<>();
+ actual.put(1, null);
+ actual.put(2, "three");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(CASE_INSENSITIVE_EQUALITY)
+ .containsEntry(3, "THREE");
+ // The test fails and does not contain the expected key, but does contain the expected value for
+ // a different key. No reasonable implementation would find this value in the second entry
+ // without hitting the exception from trying the first entry (which has a null value), so we
+ // should report the exception as well.
+ assertFailureKeys(
+ "for key",
+ "expected value",
+ "testing whether",
+ "but was missing",
+ "other keys with matching values",
+ "full map",
+ "additionally, one or more exceptions were thrown while comparing values",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, THREE) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void comparingValuesUsing_doesNotContainEntry_successExcludedKeyHasWrongValues() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "+123", "def", "+456");
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .doesNotContainEntry("def", 123);
+ }
+
+ @Test
+ public void comparingValuesUsing_doesNotContainEntry_successWrongKeyHasExcludedValue() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "+123", "def", "+456");
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .doesNotContainEntry("xyz", 456);
+ }
+
+ @Test
+ public void comparingValuesUsing_doesNotContainEntry_successMissingExcludedKeyAndValue() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456");
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .doesNotContainEntry("xyz", 321);
+ }
+
+ @Test
+ public void comparingValuesUsing_doesNotContainEntry_failure() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "+123", "def", "+456");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .doesNotContainEntry("def", 456);
+ assertFailureKeys("expected not to contain", "testing whether", "but contained", "full map");
+ assertFailureValue("expected not to contain", "def=456");
+ assertFailureValue("but contained", "def=+456");
+ assertFailureValue("full map", "{abc=+123, def=+456}");
+ }
+
+ @Test
+ public void comparingValuesUsing_doesNotContainEntry_handlesException() {
+ Map<Integer, String> actual = new LinkedHashMap<>();
+ actual.put(1, "one");
+ actual.put(2, null);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(CASE_INSENSITIVE_EQUALITY)
+ .doesNotContainEntry(2, "TWO");
+ // This test would pass if compare(null, "TWO") returned false. But it actually throws, so the
+ // test must fail.
+ assertFailureKeys(
+ "one or more exceptions were thrown while comparing values",
+ "first exception",
+ "expected not to contain",
+ "testing whether",
+ "found no match (but failing because of exception)",
+ "full map");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, TWO) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactly_success() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456");
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly("def", 456, "abc", 123);
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactly_inOrder_success() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456");
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly("abc", 123, "def", 456)
+ .inOrder();
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactly_failsExtraEntry() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly("def", 456);
+ assertFailureKeys(
+ "unexpected keys",
+ "for key",
+ "unexpected value",
+ "---",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValue("for key", "abc");
+ assertFailureValue("unexpected value", "123");
+ assertFailureValue("expected", "{def=456}");
+ assertFailureValue("testing whether", "actual value parses to expected value");
+ assertFailureValue("but was", "{abc=123, def=456}");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactly_failsMissingEntry() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly("def", 456, "xyz", 999, "abc", 123);
+ assertFailureKeys(
+ "missing keys",
+ "for key",
+ "expected value",
+ "---",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValue("for key", "xyz");
+ assertFailureValue("expected value", "999");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactly_failsWrongKey() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly("def", 456, "cab", 123);
+ assertFailureKeys(
+ "missing keys",
+ "for key",
+ "expected value",
+ "unexpected keys",
+ "for key",
+ "unexpected value",
+ "---",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValueIndexed("for key", 0, "cab");
+ assertFailureValue("expected value", "123");
+ assertFailureValueIndexed("for key", 1, "abc");
+ assertFailureValue("unexpected value", "123");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactly_failsWrongValue() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly("def", 456, "abc", 321);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValue("for key", "abc");
+ assertFailureValue("expected value", "321");
+ assertFailureValue("but got value", "123");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactly_handlesExceptions() {
+ Map<Integer, String> actual = new LinkedHashMap<>();
+ actual.put(1, "one");
+ actual.put(2, null);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(CASE_INSENSITIVE_EQUALITY)
+ .containsExactly(1, "ONE", 2, "TWO");
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing values",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, TWO) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactly_inOrder_failsOutOfOrder() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly("def", 456, "abc", 123)
+ .inOrder();
+ assertFailureKeys(
+ "entries match, but order was wrong", "expected", "testing whether", "but was");
+ assertFailureValue("expected", "{def=456, abc=123}");
+ assertFailureValue("but was", "{abc=123, def=456}");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactly_wrongValueTypeInActual() {
+ ImmutableMap<String, Object> actual = ImmutableMap.<String, Object>of("abc", "123", "def", 456);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly("def", 456, "abc", 123);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing values",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(456, 456) threw java.lang.ClassCastException");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactly_wrongValueTypeInExpected() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly("def", 456, "abc", 123L);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing values",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(123, 123) threw java.lang.ClassCastException");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactlyEntriesIn_success() {
+ ImmutableMap<String, Integer> expected = ImmutableMap.of("def", 456, "abc", 123);
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456");
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyEntriesIn(expected);
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactlyEntriesIn_inOrder_success() {
+ ImmutableMap<String, Integer> expected = ImmutableMap.of("abc", 123, "def", 456);
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456");
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyEntriesIn(expected)
+ .inOrder();
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactlyEntriesIn_failsExtraEntry() {
+ ImmutableMap<String, Integer> expected = ImmutableMap.of("def", 456);
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyEntriesIn(expected);
+ assertFailureKeys(
+ "unexpected keys",
+ "for key",
+ "unexpected value",
+ "---",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValue("for key", "abc");
+ assertFailureValue("unexpected value", "123");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactlyEntriesIn_failsMissingEntry() {
+ ImmutableMap<String, Integer> expected = ImmutableMap.of("def", 456, "xyz", 999, "abc", 123);
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyEntriesIn(expected);
+ assertFailureKeys(
+ "missing keys",
+ "for key",
+ "expected value",
+ "---",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValue("for key", "xyz");
+ assertFailureValue("expected value", "999");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactlyEntriesIn_failsWrongKey() {
+ ImmutableMap<String, Integer> expected = ImmutableMap.of("def", 456, "cab", 123);
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyEntriesIn(expected);
+ assertFailureKeys(
+ "missing keys",
+ "for key",
+ "expected value",
+ "unexpected keys",
+ "for key",
+ "unexpected value",
+ "---",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValueIndexed("for key", 0, "cab");
+ assertFailureValue("expected value", "123");
+ assertFailureValueIndexed("for key", 1, "abc");
+ assertFailureValue("unexpected value", "123");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactlyEntriesIn_failsWrongValue() {
+ ImmutableMap<String, Integer> expected = ImmutableMap.of("def", 456, "abc", 321);
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyEntriesIn(expected);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValue("for key", "abc");
+ assertFailureValue("expected value", "321");
+ assertFailureValue("but got value", "123");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactlyEntriesIn_diffMissingAndExtraAndWrongValue() {
+ ImmutableMap<String, Integer> expected = ImmutableMap.of("abc", 30, "def", 60, "ghi", 90);
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("abc", 35, "fed", 60, "ghi", 101);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(WITHIN_10_OF)
+ .containsExactlyEntriesIn(expected);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "diff",
+ "missing keys",
+ "for key",
+ "expected value",
+ "unexpected keys",
+ "for key",
+ "unexpected value",
+ "---",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValueIndexed("for key", 0, "ghi");
+ assertFailureValueIndexed("expected value", 0, "90");
+ assertFailureValue("but got value", "101");
+ assertFailureValue("diff", "11");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactlyEntriesIn_handlesFormatDiffExceptions() {
+ ImmutableMap<String, Integer> expected = ImmutableMap.of("abc", 30, "def", 60, "ghi", 90);
+ Map<String, Integer> actual = new LinkedHashMap<>();
+ actual.put("abc", 35);
+ actual.put("def", null);
+ actual.put("ghi", 95);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(WITHIN_10_OF)
+ .containsExactlyEntriesIn(expected);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing values",
+ "first exception",
+ "additionally, one or more exceptions were thrown while formatting diffs",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception", 0)
+ .startsWith("compare(null, 60) threw java.lang.NullPointerException");
+ assertThatFailure()
+ .factValue("first exception", 1)
+ .startsWith("formatDiff(null, 60) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactlyEntriesIn_inOrder_failsOutOfOrder() {
+ ImmutableMap<String, Integer> expected = ImmutableMap.of("def", 456, "abc", 123);
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyEntriesIn(expected)
+ .inOrder();
+ assertFailureKeys(
+ "entries match, but order was wrong", "expected", "testing whether", "but was");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactlyEntriesIn_empty() {
+ ImmutableMap<String, Integer> expected = ImmutableMap.of();
+ ImmutableMap<String, String> actual = ImmutableMap.of();
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyEntriesIn(expected);
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactlyEntriesIn_failsEmpty() {
+ ImmutableMap<String, Integer> expected = ImmutableMap.of();
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyEntriesIn(expected);
+ assertFailureKeys("expected to be empty", "but was");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactlyEntriesIn_wrongValueTypeInActual() {
+ ImmutableMap<String, Integer> expected = ImmutableMap.of("def", 456, "abc", 123);
+ ImmutableMap<String, Object> actual = ImmutableMap.<String, Object>of("abc", "123", "def", 456);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyEntriesIn(expected);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing values",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(456, 456) threw java.lang.ClassCastException");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeast_success() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456", "ghi", "789");
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeast("def", 456, "abc", 123);
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeast_inOrder_success() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "ghi", "789", "def", "456");
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeast("abc", 123, "def", 456)
+ .inOrder();
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeast_failsMissingEntry() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456", "ghi", "789");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeast("def", 456, "xyz", 999, "abc", 123);
+ assertFailureKeys(
+ "missing keys",
+ "for key",
+ "expected value",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was");
+ assertFailureValue("for key", "xyz");
+ assertFailureValue("expected value", "999");
+ assertFailureValue("expected to contain at least", "{def=456, xyz=999, abc=123}");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeast_failsWrongKey() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeast("def", 456, "cab", 123);
+ assertFailureKeys(
+ "missing keys",
+ "for key",
+ "expected value",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was");
+ assertFailureValue("for key", "cab");
+ assertFailureValue("expected value", "123");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeast_failsWrongValue() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeast("abc", 321);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was");
+ assertFailureValue("for key", "abc");
+ assertFailureValue("expected value", "321");
+ assertFailureValue("but got value", "123");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeast_handlesExceptions() {
+ Map<Integer, String> actual = new LinkedHashMap<>();
+ actual.put(1, "one");
+ actual.put(2, null);
+ actual.put(3, "three");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(CASE_INSENSITIVE_EQUALITY)
+ .containsAtLeast(1, "ONE", 2, "TWO");
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing values",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, TWO) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeast_inOrder_failsOutOfOrder() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456", "ghi", "789");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeast("def", 456, "abc", 123)
+ .inOrder();
+ assertFailureKeys(
+ "required entries were all found, but order was wrong",
+ "expected to contain at least",
+ "testing whether",
+ "but was");
+ assertFailureValue("expected to contain at least", "{def=456, abc=123}");
+ assertFailureValue("but was", "{abc=123, def=456, ghi=789}");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeast_wrongValueTypeInExpectedActual() {
+ ImmutableMap<String, Object> actual = ImmutableMap.<String, Object>of("abc", "123", "def", 456);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeast("def", 456);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing values",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(456, 456) threw java.lang.ClassCastException");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeast_wrongValueTypeInUnexpectedActual_success() {
+ ImmutableMap<String, Object> actual = ImmutableMap.<String, Object>of("abc", "123", "def", 456);
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeast("abc", 123);
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeast_wrongValueTypeInExpected() {
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456", "ghi", "789");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeast("def", 456, "abc", 123L);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing values",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(123, 123) threw java.lang.ClassCastException");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeastEntriesIn_success() {
+ ImmutableMap<String, Integer> expected = ImmutableMap.of("def", 456, "abc", 123);
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456", "ghi", "789");
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastEntriesIn(expected);
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeastEntriesIn_inOrder_success() {
+ ImmutableMap<String, Integer> expected = ImmutableMap.of("abc", 123, "ghi", 789);
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456", "ghi", "789");
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastEntriesIn(expected)
+ .inOrder();
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeastEntriesIn_failsMissingEntry() {
+ ImmutableMap<String, Integer> expected = ImmutableMap.of("def", 456, "xyz", 999, "abc", 123);
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456", "ghi", "789");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastEntriesIn(expected);
+ assertFailureKeys(
+ "missing keys",
+ "for key",
+ "expected value",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was");
+ assertFailureValue("for key", "xyz");
+ assertFailureValue("expected value", "999");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeastEntriesIn_failsWrongKey() {
+ ImmutableMap<String, Integer> expected = ImmutableMap.of("def", 456, "cab", 123);
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastEntriesIn(expected);
+ assertFailureKeys(
+ "missing keys",
+ "for key",
+ "expected value",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was");
+ assertFailureValue("for key", "cab");
+ assertFailureValue("expected value", "123");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeastEntriesIn_failsWrongValue() {
+ ImmutableMap<String, Integer> expected = ImmutableMap.of("def", 456, "abc", 321);
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456", "ghi", "789");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastEntriesIn(expected);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was");
+ assertFailureValue("for key", "abc");
+ assertFailureValue("expected value", "321");
+ assertFailureValue("but got value", "123");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeastEntriesIn_diffMissingAndWrongValue() {
+ ImmutableMap<String, Integer> expected = ImmutableMap.of("abc", 30, "def", 60, "ghi", 90);
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("abc", 35, "fed", 60, "ghi", 101);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(WITHIN_10_OF)
+ .containsAtLeastEntriesIn(expected);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "diff",
+ "missing keys",
+ "for key",
+ "expected value",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was");
+ assertFailureValueIndexed("for key", 0, "ghi");
+ assertFailureValueIndexed("expected value", 0, "90");
+ assertFailureValue("but got value", "101");
+ assertFailureValue("diff", "11");
+ assertFailureValueIndexed("for key", 1, "def");
+ assertFailureValueIndexed("expected value", 1, "60");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeastEntriesIn_handlesFormatDiffExceptions() {
+ ImmutableMap<String, Integer> expected = ImmutableMap.of("abc", 30, "def", 60, "ghi", 90);
+ Map<String, Integer> actual = new LinkedHashMap<>();
+ actual.put("abc", 35);
+ actual.put("def", null);
+ actual.put("ghi", 95);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(WITHIN_10_OF)
+ .containsAtLeastEntriesIn(expected);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing values",
+ "first exception",
+ "additionally, one or more exceptions were thrown while formatting diffs",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception", 0)
+ .startsWith("compare(null, 60) threw java.lang.NullPointerException");
+ assertThatFailure()
+ .factValue("first exception", 1)
+ .startsWith("formatDiff(null, 60) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeastEntriesIn_inOrder_failsOutOfOrder() {
+ ImmutableMap<String, Integer> expected = ImmutableMap.of("ghi", 789, "abc", 123);
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456", "ghi", "789");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastEntriesIn(expected)
+ .inOrder();
+ assertFailureKeys(
+ "required entries were all found, but order was wrong",
+ "expected to contain at least",
+ "testing whether",
+ "but was");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeastEntriesIn_empty() {
+ ImmutableMap<String, Integer> expected = ImmutableMap.of();
+ ImmutableMap<String, String> actual = ImmutableMap.of("abc", "123", "def", "456");
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastEntriesIn(expected);
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeastEntriesIn_wrongValueTypeInExpectedActual() {
+ ImmutableMap<String, Integer> expected = ImmutableMap.of("def", 456);
+ ImmutableMap<String, Object> actual = ImmutableMap.<String, Object>of("abc", "123", "def", 456);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastEntriesIn(expected);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing values",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(456, 456) threw java.lang.ClassCastException");
+ }
+
+ @Test
+ public void
+ comparingValuesUsing_containsAtLeastEntriesIn_wrongValueTypeInUnexpectedActual_success() {
+ ImmutableMap<String, Integer> expected = ImmutableMap.of("abc", 123);
+ ImmutableMap<String, Object> actual = ImmutableMap.<String, Object>of("abc", "123", "def", 456);
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastEntriesIn(expected);
+ }
+
+ @Test
+ public void formattingDiffsUsing_success() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("ghi", 300, "def", 200, "abc", 100);
+ assertThat(actual)
+ .formattingDiffsUsing(INT_DIFF_FORMATTER)
+ .containsExactly("abc", 100, "def", 200, "ghi", 300);
+ }
+
+ @Test
+ public void formattingDiffsUsing_failure() {
+ ImmutableMap<String, Integer> actual = ImmutableMap.of("ghi", 300, "def", 201, "abc", 100);
+ expectFailure
+ .whenTesting()
+ .that(actual)
+ .formattingDiffsUsing(INT_DIFF_FORMATTER)
+ .containsExactly("abc", 100, "def", 200, "ghi", 300);
+ assertFailureKeys(
+ "keys with wrong values",
+ "for key",
+ "expected value",
+ "but got value",
+ "diff",
+ "---",
+ "expected",
+ "but was");
+ assertFailureValue("expected value", "200");
+ assertFailureValue("but got value", "201");
+ assertFailureValue("diff", "1");
+ }
+
+ private MapSubject expectFailureWhenTestingThat(Map<?, ?> actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/MathUtilTest.java b/core/src/test/java/com/google/common/truth/MathUtilTest.java
new file mode 100644
index 00000000..292bacd8
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/MathUtilTest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.MathUtil.equalWithinTolerance;
+import static com.google.common.truth.MathUtil.notEqualWithinTolerance;
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link MathUtil} used by numeric subjects.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+@RunWith(JUnit4.class)
+public class MathUtilTest {
+ @Test
+ public void floatEquals() {
+ assertThat(equalWithinTolerance(1.3f, 1.3f, 0.00000000000001f)).isTrue();
+ assertThat(equalWithinTolerance(1.3f, 1.3f, 0.0f)).isTrue();
+ assertThat(equalWithinTolerance(0.0f, 1.0f + 2.0f - 3.0f, 0.00000000000000000000000000000001f))
+ .isTrue();
+ assertThat(equalWithinTolerance(1.3f, 1.303f, 0.004f)).isTrue();
+ assertThat(equalWithinTolerance(1.3f, 1.303f, 0.002f)).isFalse();
+ assertThat(equalWithinTolerance(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, 0.01f))
+ .isFalse();
+ assertThat(equalWithinTolerance(Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY, 0.01f))
+ .isFalse();
+ assertThat(equalWithinTolerance(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY, 0.01f))
+ .isFalse();
+ assertThat(equalWithinTolerance(Float.NaN, Float.NaN, 0.01f)).isFalse();
+ }
+
+ @Test
+ public void doubleEquals() {
+ assertThat(equalWithinTolerance(1.3d, 1.3d, 0.00000000000001d)).isTrue();
+ assertThat(equalWithinTolerance(1.3d, 1.3d, 0.0d)).isTrue();
+ assertThat(equalWithinTolerance(0.0d, 1.0d + 2.0d - 3.0d, 0.00000000000000000000000000000001d))
+ .isTrue();
+ assertThat(equalWithinTolerance(1.3d, 1.303d, 0.004d)).isTrue();
+ assertThat(equalWithinTolerance(1.3d, 1.303d, 0.002d)).isFalse();
+ assertThat(equalWithinTolerance(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, 0.01d))
+ .isFalse();
+ assertThat(equalWithinTolerance(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 0.01d))
+ .isFalse();
+ assertThat(equalWithinTolerance(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, 0.01d))
+ .isFalse();
+ assertThat(equalWithinTolerance(Double.NaN, Double.NaN, 0.01d)).isFalse();
+ }
+
+ @Test
+ public void floatNotEquals() {
+ assertThat(notEqualWithinTolerance(1.3f, 1.3f, 0.00000000000001f)).isFalse();
+ assertThat(notEqualWithinTolerance(1.3f, 1.3f, 0.0f)).isFalse();
+ assertThat(
+ notEqualWithinTolerance(0.0f, 1.0f + 2.0f - 3.0f, 0.00000000000000000000000000000001f))
+ .isFalse();
+ assertThat(notEqualWithinTolerance(1.3f, 1.303f, 0.004f)).isFalse();
+ assertThat(notEqualWithinTolerance(1.3f, 1.303f, 0.002f)).isTrue();
+ assertThat(notEqualWithinTolerance(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, 0.01f))
+ .isFalse();
+ assertThat(notEqualWithinTolerance(Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY, 0.01f))
+ .isFalse();
+ assertThat(notEqualWithinTolerance(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY, 0.01f))
+ .isFalse();
+ assertThat(notEqualWithinTolerance(Float.NaN, Float.NaN, 0.01f)).isFalse();
+ }
+
+ @Test
+ public void doubleNotEquals() {
+ assertThat(notEqualWithinTolerance(1.3d, 1.3d, 0.00000000000001d)).isFalse();
+ assertThat(notEqualWithinTolerance(1.3d, 1.3d, 0.0d)).isFalse();
+ assertThat(
+ notEqualWithinTolerance(0.0d, 1.0d + 2.0d - 3.0d, 0.00000000000000000000000000000001d))
+ .isFalse();
+ assertThat(notEqualWithinTolerance(1.3d, 1.303d, 0.004d)).isFalse();
+ assertThat(notEqualWithinTolerance(1.3d, 1.303d, 0.002d)).isTrue();
+ assertThat(notEqualWithinTolerance(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, 0.01d))
+ .isFalse();
+ assertThat(notEqualWithinTolerance(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 0.01d))
+ .isFalse();
+ assertThat(notEqualWithinTolerance(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, 0.01d))
+ .isFalse();
+ assertThat(notEqualWithinTolerance(Double.NaN, Double.NaN, 0.01d)).isFalse();
+ }
+
+ @Test
+ public void equalsDifferentTypes() {
+ assertThat(equalWithinTolerance(1.3d, 1.3f, 0.00000000000001d)).isFalse();
+ assertThat(equalWithinTolerance(1.3f, 1.3d, 0.00000000000001f)).isFalse();
+ }
+
+ // TODO(user): More complicated ways to break float/double casting to make sure.
+
+}
diff --git a/core/src/test/java/com/google/common/truth/MultimapSubjectTest.java b/core/src/test/java/com/google/common/truth/MultimapSubjectTest.java
new file mode 100644
index 00000000..b638d9a4
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/MultimapSubjectTest.java
@@ -0,0 +1,1879 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.TestCorrespondences.CASE_INSENSITIVE_EQUALITY;
+import static com.google.common.truth.TestCorrespondences.CASE_INSENSITIVE_EQUALITY_HALF_NULL_SAFE;
+import static com.google.common.truth.TestCorrespondences.STRING_PARSES_TO_INTEGER_CORRESPONDENCE;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimap;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Multimap Subjects.
+ *
+ * @author Daniel Ploch
+ * @author Kurt Alfred Kluever
+ */
+@RunWith(JUnit4.class)
+public class MultimapSubjectTest extends BaseSubjectTestCase {
+
+ @Test
+ public void listMultimapIsEqualTo_passes() {
+ ImmutableListMultimap<String, String> multimapA =
+ ImmutableListMultimap.<String, String>builder()
+ .putAll("kurt", "kluever", "russell", "cobain")
+ .build();
+ ImmutableListMultimap<String, String> multimapB =
+ ImmutableListMultimap.<String, String>builder()
+ .putAll("kurt", "kluever", "russell", "cobain")
+ .build();
+
+ assertThat(multimapA.equals(multimapB)).isTrue();
+
+ assertThat(multimapA).isEqualTo(multimapB);
+ }
+
+ @Test
+ public void listMultimapIsEqualTo_fails() {
+ ImmutableListMultimap<String, String> multimapA =
+ ImmutableListMultimap.<String, String>builder()
+ .putAll("kurt", "kluever", "russell", "cobain")
+ .build();
+ ImmutableListMultimap<String, String> multimapB =
+ ImmutableListMultimap.<String, String>builder()
+ .putAll("kurt", "kluever", "cobain", "russell")
+ .build();
+
+ expectFailureWhenTestingThat(multimapA).isEqualTo(multimapB);
+ assertFailureKeys(
+ "contents match, but order was wrong",
+ "keys with out-of-order values",
+ "---",
+ "expected",
+ "but was");
+ assertFailureValue("keys with out-of-order values", "[kurt]");
+ assertFailureValue("expected", "{kurt=[kluever, cobain, russell]}");
+ assertFailureValue("but was", "{kurt=[kluever, russell, cobain]}");
+ }
+
+ @Test
+ public void setMultimapIsEqualTo_passes() {
+ ImmutableSetMultimap<String, String> multimapA =
+ ImmutableSetMultimap.<String, String>builder()
+ .putAll("kurt", "kluever", "russell", "cobain")
+ .build();
+ ImmutableSetMultimap<String, String> multimapB =
+ ImmutableSetMultimap.<String, String>builder()
+ .putAll("kurt", "kluever", "cobain", "russell")
+ .build();
+
+ assertThat(multimapA.equals(multimapB)).isTrue();
+
+ assertThat(multimapA).isEqualTo(multimapB);
+ }
+
+ @Test
+ public void setMultimapIsEqualTo_fails() {
+ ImmutableSetMultimap<String, String> multimapA =
+ ImmutableSetMultimap.<String, String>builder()
+ .putAll("kurt", "kluever", "russell", "cobain")
+ .build();
+ ImmutableSetMultimap<String, String> multimapB =
+ ImmutableSetMultimap.<String, String>builder().putAll("kurt", "kluever", "russell").build();
+
+ expectFailureWhenTestingThat(multimapA).isEqualTo(multimapB);
+ assertFailureKeys("unexpected", "---", "expected", "but was");
+ assertFailureValue("unexpected", "{kurt=[cobain]}");
+ assertFailureValue("expected", "{kurt=[kluever, russell]}");
+ assertFailureValue("but was", "{kurt=[kluever, russell, cobain]}");
+ }
+
+ @Test
+ public void setMultimapIsEqualToListMultimap_fails() {
+ ImmutableSetMultimap<String, String> multimapA =
+ ImmutableSetMultimap.<String, String>builder()
+ .putAll("kurt", "kluever", "russell", "cobain")
+ .build();
+ ImmutableListMultimap<String, String> multimapB =
+ ImmutableListMultimap.<String, String>builder()
+ .putAll("kurt", "kluever", "russell", "cobain")
+ .build();
+ expectFailureWhenTestingThat(multimapA).isEqualTo(multimapB);
+ assertFailureKeys(
+ "expected",
+ "an instance of",
+ "but was",
+ "an instance of",
+ "a SetMultimap cannot equal a ListMultimap if either is non-empty");
+ assertFailureValueIndexed("an instance of", 0, "ListMultimap");
+ assertFailureValueIndexed("an instance of", 1, "SetMultimap");
+ }
+
+ @Test
+ public void isEqualTo_failsWithSameToString() {
+ expectFailureWhenTestingThat(ImmutableMultimap.of(1, "a", 1, "b", 2, "c"))
+ .isEqualTo(ImmutableMultimap.of(1L, "a", 1L, "b", 2L, "c"));
+ assertFailureKeys("missing", "unexpected", "---", "expected", "but was");
+ assertFailureValue("missing", "[1=a, 1=b, 2=c] (Map.Entry<java.lang.Long, java.lang.String>)");
+ assertFailureValue(
+ "unexpected", "[1=a, 1=b, 2=c] (Map.Entry<java.lang.Integer, java.lang.String>)");
+ assertFailureValue("expected", "{1=[a, b], 2=[c]}");
+ assertFailureValue("but was", "{1=[a, b], 2=[c]}");
+ }
+
+ @Test
+ public void multimapIsEmpty() {
+ ImmutableMultimap<String, String> multimap = ImmutableMultimap.of();
+ assertThat(multimap).isEmpty();
+ }
+
+ @Test
+ public void multimapIsEmptyWithFailure() {
+ ImmutableMultimap<Integer, Integer> multimap = ImmutableMultimap.of(1, 5);
+ expectFailureWhenTestingThat(multimap).isEmpty();
+ assertFailureKeys("expected to be empty", "but was");
+ }
+
+ @Test
+ public void multimapIsNotEmpty() {
+ ImmutableMultimap<Integer, Integer> multimap = ImmutableMultimap.of(1, 5);
+ assertThat(multimap).isNotEmpty();
+ }
+
+ @Test
+ public void multimapIsNotEmptyWithFailure() {
+ ImmutableMultimap<Integer, Integer> multimap = ImmutableMultimap.of();
+ expectFailureWhenTestingThat(multimap).isNotEmpty();
+ assertFailureKeys("expected not to be empty");
+ }
+
+ @Test
+ public void hasSize() {
+ assertThat(ImmutableMultimap.of(1, 2, 3, 4)).hasSize(2);
+ }
+
+ @Test
+ public void hasSizeZero() {
+ assertThat(ImmutableMultimap.of()).hasSize(0);
+ }
+
+ @Test
+ public void hasSizeNegative() {
+ try {
+ assertThat(ImmutableMultimap.of(1, 2)).hasSize(-1);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test
+ public void containsKey() {
+ ImmutableMultimap<String, String> multimap = ImmutableMultimap.of("kurt", "kluever");
+ assertThat(multimap).containsKey("kurt");
+ }
+
+ @Test
+ public void containsKeyFailure() {
+ ImmutableMultimap<String, String> multimap = ImmutableMultimap.of("kurt", "kluever");
+ expectFailureWhenTestingThat(multimap).containsKey("daniel");
+ assertFailureKeys("value of", "expected to contain", "but was", "multimap was");
+ assertFailureValue("value of", "multimap.keySet()");
+ assertFailureValue("expected to contain", "daniel");
+ assertFailureValue("but was", "[kurt]");
+ }
+
+ @Test
+ public void containsKeyNull() {
+ Multimap<String, String> multimap = HashMultimap.create();
+ multimap.put(null, "null");
+ assertThat(multimap).containsKey(null);
+ }
+
+ @Test
+ public void containsKeyNullFailure() {
+ ImmutableMultimap<String, String> multimap = ImmutableMultimap.of("kurt", "kluever");
+ expectFailureWhenTestingThat(multimap).containsKey(null);
+ assertFailureKeys("value of", "expected to contain", "but was", "multimap was");
+ assertFailureValue("value of", "multimap.keySet()");
+ assertFailureValue("expected to contain", "null");
+ assertFailureValue("but was", "[kurt]");
+ }
+
+ @Test
+ public void containsKey_failsWithSameToString() {
+ expectFailureWhenTestingThat(
+ ImmutableMultimap.of(1L, "value1a", 1L, "value1b", 2L, "value2", "1", "value3"))
+ .containsKey(1);
+ assertFailureKeys(
+ "value of",
+ "expected to contain",
+ "an instance of",
+ "but did not",
+ "though it did contain",
+ "full contents",
+ "multimap was");
+ assertFailureValue("value of", "multimap.keySet()");
+ assertFailureValue("expected to contain", "1");
+ }
+
+ @Test
+ public void doesNotContainKey() {
+ ImmutableMultimap<String, String> multimap = ImmutableMultimap.of("kurt", "kluever");
+ assertThat(multimap).doesNotContainKey("daniel");
+ assertThat(multimap).doesNotContainKey(null);
+ }
+
+ @Test
+ public void doesNotContainKeyFailure() {
+ ImmutableMultimap<String, String> multimap = ImmutableMultimap.of("kurt", "kluever");
+ expectFailureWhenTestingThat(multimap).doesNotContainKey("kurt");
+ assertFailureKeys("value of", "expected not to contain", "but was", "multimap was");
+ assertFailureValue("value of", "multimap.keySet()");
+ assertFailureValue("expected not to contain", "kurt");
+ assertFailureValue("but was", "[kurt]");
+ }
+
+ @Test
+ public void doesNotContainNullKeyFailure() {
+ Multimap<String, String> multimap = HashMultimap.create();
+ multimap.put(null, "null");
+ expectFailureWhenTestingThat(multimap).doesNotContainKey(null);
+ assertFailureKeys("value of", "expected not to contain", "but was", "multimap was");
+ assertFailureValue("value of", "multimap.keySet()");
+ assertFailureValue("expected not to contain", "null");
+ assertFailureValue("but was", "[null]");
+ }
+
+ @Test
+ public void containsEntry() {
+ ImmutableMultimap<String, String> multimap = ImmutableMultimap.of("kurt", "kluever");
+ assertThat(multimap).containsEntry("kurt", "kluever");
+ }
+
+ @Test
+ public void containsEntryFailure() {
+ ImmutableMultimap<String, String> multimap = ImmutableMultimap.of("kurt", "kluever");
+ expectFailureWhenTestingThat(multimap).containsEntry("daniel", "ploch");
+ assertFailureKeys("expected to contain entry", "but was");
+ assertFailureValue("expected to contain entry", "daniel=ploch");
+ assertFailureValue("but was", "{kurt=[kluever]}");
+ }
+
+ @Test
+ public void containsEntryWithNullValueNullExpected() {
+ ListMultimap<String, String> actual = ArrayListMultimap.create();
+ actual.put("a", null);
+ assertThat(actual).containsEntry("a", null);
+ }
+
+ @Test
+ public void failContainsEntry() {
+ ImmutableMultimap<String, String> actual = ImmutableMultimap.of("a", "A");
+ expectFailureWhenTestingThat(actual).containsEntry("b", "B");
+ assertFailureKeys("expected to contain entry", "but was");
+ assertFailureValue("expected to contain entry", "b=B");
+ assertFailureValue("but was", "{a=[A]}");
+ }
+
+ @Test
+ public void failContainsEntryFailsWithWrongValueForKey() {
+ ImmutableMultimap<String, String> actual = ImmutableMultimap.of("a", "A");
+ expectFailureWhenTestingThat(actual).containsEntry("a", "a");
+ assertFailureKeys(
+ "expected to contain entry",
+ "but did not",
+ "though it did contain values with that key",
+ "full contents");
+ assertFailureValue("though it did contain values with that key", "[A]");
+ }
+
+ @Test
+ public void failContainsEntryWithNullValuePresentExpected() {
+ ListMultimap<String, String> actual = ArrayListMultimap.create();
+ actual.put("a", null);
+ expectFailureWhenTestingThat(actual).containsEntry("a", "A");
+ assertFailureKeys(
+ "expected to contain entry",
+ "but did not",
+ "though it did contain values with that key",
+ "full contents");
+ assertFailureValue("though it did contain values with that key", "[null]");
+ }
+
+ @Test
+ public void failContainsEntryWithPresentValueNullExpected() {
+ ImmutableMultimap<String, String> actual = ImmutableMultimap.of("a", "A");
+ expectFailureWhenTestingThat(actual).containsEntry("a", null);
+ assertFailureKeys(
+ "expected to contain entry",
+ "but did not",
+ "though it did contain values with that key",
+ "full contents");
+ assertFailureValue("expected to contain entry", "a=null");
+ }
+
+ @Test
+ public void failContainsEntryFailsWithWrongKeyForValue() {
+ ImmutableMultimap<String, String> actual = ImmutableMultimap.of("a", "A");
+ expectFailureWhenTestingThat(actual).containsEntry("b", "A");
+ assertFailureKeys(
+ "expected to contain entry",
+ "but did not",
+ "though it did contain keys with that value",
+ "full contents");
+ assertFailureValue("though it did contain keys with that value", "[a]");
+ }
+
+ @Test
+ public void containsEntry_failsWithSameToString() throws Exception {
+ expectFailureWhenTestingThat(
+ ImmutableMultimap.builder().put(1, "1").put(1, 1L).put(1L, 1).put(2, 3).build())
+ .containsEntry(1, 1);
+ assertFailureKeys(
+ "expected to contain entry",
+ "an instance of",
+ "but did not",
+ "though it did contain",
+ "full contents");
+ assertFailureValue("expected to contain entry", "1=1");
+ assertFailureValue("an instance of", "Map.Entry<java.lang.Integer, java.lang.Integer>");
+ assertFailureValue(
+ "though it did contain",
+ "[1=1 (Map.Entry<java.lang.Integer, java.lang.String>), "
+ + "1=1 (Map.Entry<java.lang.Integer, java.lang.Long>), "
+ + "1=1 (Map.Entry<java.lang.Long, java.lang.Integer>)]");
+ }
+
+ @Test
+ public void doesNotContainEntry() {
+ ImmutableMultimap<String, String> multimap = ImmutableMultimap.of("kurt", "kluever");
+ assertThat(multimap).doesNotContainEntry("daniel", "ploch");
+ }
+
+ @Test
+ public void doesNotContainEntryFailure() {
+ ImmutableMultimap<String, String> multimap = ImmutableMultimap.of("kurt", "kluever");
+ expectFailureWhenTestingThat(multimap).doesNotContainEntry("kurt", "kluever");
+ assertFailureKeys("value of", "expected not to contain", "but was");
+ assertFailureValue("value of", "multimap.entries()");
+ assertFailureValue("expected not to contain", "kurt=kluever");
+ assertFailureValue("but was", "[kurt=kluever]");
+ }
+
+ @Test
+ public void valuesForKey() {
+ ImmutableMultimap<Integer, String> multimap =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+
+ assertThat(multimap).valuesForKey(3).hasSize(3);
+ assertThat(multimap).valuesForKey(4).containsExactly("four", "five");
+ assertThat(multimap).valuesForKey(3).containsAtLeast("one", "six").inOrder();
+ assertThat(multimap).valuesForKey(5).isEmpty();
+ }
+
+ @Test
+ public void valuesForKeyListMultimap() {
+ ImmutableListMultimap<Integer, String> multimap =
+ ImmutableListMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+
+ assertThat(multimap).valuesForKey(4).isInStrictOrder();
+ }
+
+ @Test
+ public void containsExactlyEntriesIn() {
+ ImmutableListMultimap<Integer, String> listMultimap =
+ ImmutableListMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+ ImmutableSetMultimap<Integer, String> setMultimap = ImmutableSetMultimap.copyOf(listMultimap);
+
+ assertThat(listMultimap).containsExactlyEntriesIn(setMultimap);
+ }
+
+ @Test
+ public void containsExactlyNoArg() {
+ ImmutableMultimap<Integer, String> actual = ImmutableMultimap.of();
+
+ assertThat(actual).containsExactly();
+ assertThat(actual).containsExactly().inOrder();
+
+ expectFailureWhenTestingThat(ImmutableMultimap.of(42, "Answer", 42, "6x7")).containsExactly();
+ assertFailureKeys("expected to be empty", "but was");
+ }
+
+ @Test
+ public void containsExactlyEmpty() {
+ ImmutableListMultimap<Integer, String> actual = ImmutableListMultimap.of();
+ ImmutableSetMultimap<Integer, String> expected = ImmutableSetMultimap.of();
+
+ assertThat(actual).containsExactlyEntriesIn(expected);
+ assertThat(actual).containsExactlyEntriesIn(expected).inOrder();
+ }
+
+ @Test
+ public void containsExactlyRejectsNull() {
+ ImmutableMultimap<Integer, String> multimap =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+
+ try {
+ assertThat(multimap).containsExactlyEntriesIn(null);
+ fail("Should have thrown.");
+ } catch (NullPointerException expected) {
+ }
+ }
+
+ @Test
+ public void containsExactlyRespectsDuplicates() {
+ ImmutableListMultimap<Integer, String> actual =
+ ImmutableListMultimap.of(3, "one", 3, "two", 3, "one", 4, "five", 4, "five");
+ ImmutableListMultimap<Integer, String> expected =
+ ImmutableListMultimap.of(3, "two", 4, "five", 3, "one", 4, "five", 3, "one");
+
+ assertThat(actual).containsExactlyEntriesIn(expected);
+ }
+
+ @Test
+ public void containsExactlyRespectsDuplicatesFailure() {
+ ImmutableListMultimap<Integer, String> actual =
+ ImmutableListMultimap.of(3, "one", 3, "two", 3, "one", 4, "five", 4, "five");
+ ImmutableSetMultimap<Integer, String> expected = ImmutableSetMultimap.copyOf(actual);
+
+ expectFailureWhenTestingThat(actual).containsExactlyEntriesIn(expected);
+ assertFailureKeys("unexpected", "---", "expected", "but was");
+ assertFailureValue("unexpected", "{3=[one], 4=[five]}");
+ assertFailureValue("expected", "{3=[one, two], 4=[five]}");
+ assertFailureValue("but was", "{3=[one, two, one], 4=[five, five]}");
+ }
+
+ @Test
+ public void containsExactlyFailureMissing() {
+ ImmutableMultimap<Integer, String> expected =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+ ListMultimap<Integer, String> actual = LinkedListMultimap.create(expected);
+ actual.remove(3, "six");
+ actual.remove(4, "five");
+
+ expectFailureWhenTestingThat(actual).containsExactlyEntriesIn(expected);
+ assertFailureKeys("missing", "---", "expected", "but was");
+ assertFailureValue("missing", "{3=[six], 4=[five]}");
+ }
+
+ @Test
+ public void containsExactlyFailureExtra() {
+ ImmutableMultimap<Integer, String> expected =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+ ListMultimap<Integer, String> actual = LinkedListMultimap.create(expected);
+ actual.put(4, "nine");
+ actual.put(5, "eight");
+
+ expectFailureWhenTestingThat(actual).containsExactlyEntriesIn(expected);
+ assertFailureKeys("unexpected", "---", "expected", "but was");
+ assertFailureValue("unexpected", "{4=[nine], 5=[eight]}");
+ }
+
+ @Test
+ public void containsExactlyFailureBoth() {
+ ImmutableMultimap<Integer, String> expected =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+ ListMultimap<Integer, String> actual = LinkedListMultimap.create(expected);
+ actual.remove(3, "six");
+ actual.remove(4, "five");
+ actual.put(4, "nine");
+ actual.put(5, "eight");
+
+ expectFailureWhenTestingThat(actual).containsExactlyEntriesIn(expected);
+ assertFailureKeys("missing", "unexpected", "---", "expected", "but was");
+ assertFailureValue("missing", "{3=[six], 4=[five]}");
+ assertFailureValue("unexpected", "{4=[nine], 5=[eight]}");
+ }
+
+ @Test
+ public void containsExactlyFailureWithEmptyStringMissing() {
+ expectFailureWhenTestingThat(ImmutableMultimap.of()).containsExactly("", "a");
+ assertFailureKeys("missing", "---", "expected", "but was");
+ assertFailureValue("missing", "{\"\" (empty String)=[a]}");
+ assertFailureValue("expected", "{\"\" (empty String)=[a]}");
+ assertFailureValue("but was", "{}");
+ }
+
+ @Test
+ public void containsExactlyFailureWithEmptyStringExtra() {
+ expectFailureWhenTestingThat(ImmutableMultimap.of("a", "", "", "")).containsExactly("a", "");
+ assertFailureKeys("unexpected", "---", "expected", "but was");
+ assertFailureValue("unexpected", "{\"\" (empty String)=[\"\" (empty String)]}");
+ assertFailureValue("expected", "{a=[\"\" (empty String)]}");
+ assertFailureValue("but was", "{a=[], =[]}");
+ }
+
+ @Test
+ public void containsExactlyFailureWithEmptyStringBoth() {
+ expectFailureWhenTestingThat(ImmutableMultimap.of("a", "")).containsExactly("", "a");
+ assertFailureKeys("missing", "unexpected", "---", "expected", "but was");
+ assertFailureValue("missing", "{\"\" (empty String)=[a]}");
+ assertFailureValue("unexpected", "{a=[\"\" (empty String)]}");
+ assertFailureValue("expected", "{\"\" (empty String)=[a]}");
+ assertFailureValue("but was", "{a=[]}");
+ }
+
+ @Test
+ public void containsExactlyInOrder() {
+ ImmutableMultimap<Integer, String> actual =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+ ImmutableMultimap<Integer, String> expected =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+
+ assertThat(actual).containsExactlyEntriesIn(expected).inOrder();
+ }
+
+ @Test
+ public void containsExactlyInOrderDifferentTypes() {
+ ImmutableListMultimap<Integer, String> listMultimap =
+ ImmutableListMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+ ImmutableSetMultimap<Integer, String> setMultimap = ImmutableSetMultimap.copyOf(listMultimap);
+
+ assertThat(listMultimap).containsExactlyEntriesIn(setMultimap).inOrder();
+ }
+
+ @Test
+ public void containsExactlyInOrderFailure() {
+ ImmutableMultimap<Integer, String> actual =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+ ImmutableMultimap<Integer, String> expected =
+ ImmutableMultimap.of(4, "four", 3, "six", 4, "five", 3, "two", 3, "one");
+
+ assertThat(actual).containsExactlyEntriesIn(expected);
+ expectFailureWhenTestingThat(actual).containsExactlyEntriesIn(expected).inOrder();
+ assertFailureKeys(
+ "contents match, but order was wrong",
+ "keys are not in order",
+ "keys with out-of-order values",
+ "---",
+ "expected",
+ "but was");
+ assertFailureValue("keys with out-of-order values", "[4, 3]");
+ }
+
+ @Test
+ public void containsExactlyInOrderFailureValuesOnly() {
+ ImmutableMultimap<Integer, String> actual =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+ ImmutableMultimap<Integer, String> expected =
+ ImmutableMultimap.of(3, "six", 3, "two", 3, "one", 4, "five", 4, "four");
+
+ assertThat(actual).containsExactlyEntriesIn(expected);
+ expectFailureWhenTestingThat(actual).containsExactlyEntriesIn(expected).inOrder();
+ assertFailureKeys(
+ "contents match, but order was wrong",
+ "keys with out-of-order values",
+ "---",
+ "expected",
+ "but was");
+ assertFailureValue("keys with out-of-order values", "[3]");
+ }
+
+ @Test
+ public void containsExactlyVararg() {
+ ImmutableListMultimap<Integer, String> listMultimap =
+ ImmutableListMultimap.of(1, "one", 3, "six", 3, "two");
+
+ assertThat(listMultimap).containsExactly(1, "one", 3, "six", 3, "two");
+ }
+
+ @Test
+ public void containsExactlyVarargWithNull() {
+ Multimap<Integer, String> listMultimap =
+ LinkedListMultimap.create(ImmutableListMultimap.of(1, "one", 3, "six", 3, "two"));
+ listMultimap.put(4, null);
+
+ assertThat(listMultimap).containsExactly(1, "one", 3, "six", 3, "two", 4, null);
+ }
+
+ @Test
+ public void containsExactlyVarargFailureMissing() {
+ ImmutableMultimap<Integer, String> expected =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+ ListMultimap<Integer, String> actual = LinkedListMultimap.create(expected);
+ actual.remove(3, "six");
+ actual.remove(4, "five");
+
+ expectFailureWhenTestingThat(actual)
+ .containsExactly(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+ assertFailureKeys("missing", "---", "expected", "but was");
+ assertFailureValue("missing", "{3=[six], 4=[five]}");
+ assertFailureValue("expected", "{3=[one, six, two], 4=[five, four]}");
+ assertFailureValue("but was", "{3=[one, two], 4=[four]}");
+ }
+
+ @Test
+ public void containsExactlyVarargFailureExtra() {
+ ImmutableMultimap<Integer, String> expected =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+ ListMultimap<Integer, String> actual = LinkedListMultimap.create(expected);
+ actual.put(4, "nine");
+ actual.put(5, "eight");
+
+ expectFailureWhenTestingThat(actual)
+ .containsExactly(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+ assertFailureKeys("unexpected", "---", "expected", "but was");
+ assertFailureValue("unexpected", "{4=[nine], 5=[eight]}");
+ }
+
+ @Test
+ public void containsExactlyVarargFailureBoth() {
+ ImmutableMultimap<Integer, String> expected =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+ ListMultimap<Integer, String> actual = LinkedListMultimap.create(expected);
+ actual.remove(3, "six");
+ actual.remove(4, "five");
+ actual.put(4, "nine");
+ actual.put(5, "eight");
+
+ expectFailureWhenTestingThat(actual)
+ .containsExactly(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+ assertFailureKeys("missing", "unexpected", "---", "expected", "but was");
+ assertFailureValue("missing", "{3=[six], 4=[five]}");
+ assertFailureValue("unexpected", "{4=[nine], 5=[eight]}");
+ }
+
+ @Test
+ public void containsExactlyVarargRespectsDuplicates() {
+ ImmutableListMultimap<Integer, String> actual =
+ ImmutableListMultimap.of(3, "one", 3, "two", 3, "one", 4, "five", 4, "five");
+
+ assertThat(actual).containsExactly(3, "two", 4, "five", 3, "one", 4, "five", 3, "one");
+ }
+
+ @Test
+ public void containsExactlyVarargRespectsDuplicatesFailure() {
+ ImmutableListMultimap<Integer, String> actual =
+ ImmutableListMultimap.of(3, "one", 3, "two", 3, "one", 4, "five", 4, "five");
+
+ expectFailureWhenTestingThat(actual).containsExactly(3, "one", 3, "two", 4, "five");
+ assertFailureKeys("unexpected", "---", "expected", "but was");
+ assertFailureValue("unexpected", "{3=[one], 4=[five]}");
+ }
+
+ @Test
+ public void containsExactlyVarargInOrder() {
+ ImmutableMultimap<Integer, String> actual =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+
+ assertThat(actual)
+ .containsExactly(3, "one", 3, "six", 3, "two", 4, "five", 4, "four")
+ .inOrder();
+ }
+
+ @Test
+ public void containsExactlyVarargInOrderFailure() {
+ ImmutableMultimap<Integer, String> actual =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+
+ assertThat(actual).containsExactly(4, "four", 3, "six", 4, "five", 3, "two", 3, "one");
+ expectFailureWhenTestingThat(actual)
+ .containsExactly(4, "four", 3, "six", 4, "five", 3, "two", 3, "one")
+ .inOrder();
+ assertFailureKeys(
+ "contents match, but order was wrong",
+ "keys are not in order",
+ "keys with out-of-order values",
+ "---",
+ "expected",
+ "but was");
+ assertFailureValue("keys with out-of-order values", "[4, 3]");
+ }
+
+ @Test
+ public void containsExactlyVarargInOrderFailureValuesOnly() {
+ ImmutableMultimap<Integer, String> actual =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+
+ assertThat(actual).containsExactly(3, "six", 3, "two", 3, "one", 4, "five", 4, "four");
+ expectFailureWhenTestingThat(actual)
+ .containsExactly(3, "six", 3, "two", 3, "one", 4, "five", 4, "four")
+ .inOrder();
+ assertFailureKeys(
+ "contents match, but order was wrong",
+ "keys with out-of-order values",
+ "---",
+ "expected",
+ "but was");
+ assertFailureValue("keys with out-of-order values", "[3]");
+ }
+
+ @Test
+ public void containsExactlyEntriesIn_homogeneousMultimap_failsWithSameToString()
+ throws Exception {
+ expectFailureWhenTestingThat(ImmutableMultimap.of(1, "a", 1, "b", 2, "c"))
+ .containsExactlyEntriesIn(ImmutableMultimap.of(1L, "a", 1L, "b", 2L, "c"));
+ assertFailureKeys("missing", "unexpected", "---", "expected", "but was");
+ assertFailureValue("missing", "[1=a, 1=b, 2=c] (Map.Entry<java.lang.Long, java.lang.String>)");
+ assertFailureValue(
+ "unexpected", "[1=a, 1=b, 2=c] (Map.Entry<java.lang.Integer, java.lang.String>)");
+ }
+
+ @Test
+ public void containsExactlyEntriesIn_heterogeneousMultimap_failsWithSameToString()
+ throws Exception {
+ expectFailureWhenTestingThat(ImmutableMultimap.of(1, "a", 1, "b", 2L, "c"))
+ .containsExactlyEntriesIn(ImmutableMultimap.of(1L, "a", 1L, "b", 2, "c"));
+ assertFailureKeys("missing", "unexpected", "---", "expected", "but was");
+ assertFailureValue(
+ "missing",
+ "[1=a (Map.Entry<java.lang.Long, java.lang.String>), "
+ + "1=b (Map.Entry<java.lang.Long, java.lang.String>), "
+ + "2=c (Map.Entry<java.lang.Integer, java.lang.String>)]");
+ assertFailureValue(
+ "unexpected",
+ "[1=a (Map.Entry<java.lang.Integer, java.lang.String>), "
+ + "1=b (Map.Entry<java.lang.Integer, java.lang.String>), "
+ + "2=c (Map.Entry<java.lang.Long, java.lang.String>)]");
+ }
+
+ @Test
+ public void containsAtLeastEntriesIn() {
+ ImmutableListMultimap<Integer, String> actual =
+ ImmutableListMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+ ImmutableSetMultimap<Integer, String> expected =
+ ImmutableSetMultimap.of(3, "one", 3, "six", 3, "two", 4, "five");
+
+ assertThat(actual).containsAtLeastEntriesIn(expected);
+ }
+
+ @Test
+ public void containsAtLeastEmpty() {
+ ImmutableListMultimap<Integer, String> actual = ImmutableListMultimap.of(3, "one");
+ ImmutableSetMultimap<Integer, String> expected = ImmutableSetMultimap.of();
+
+ assertThat(actual).containsAtLeastEntriesIn(expected);
+ assertThat(actual).containsAtLeastEntriesIn(expected).inOrder();
+ }
+
+ @Test
+ public void containsAtLeastRejectsNull() {
+ ImmutableMultimap<Integer, String> multimap =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+
+ try {
+ assertThat(multimap).containsAtLeastEntriesIn(null);
+ fail("Should have thrown.");
+ } catch (NullPointerException expected) {
+ }
+ }
+
+ @Test
+ public void containsAtLeastRespectsDuplicates() {
+ ImmutableListMultimap<Integer, String> actual =
+ ImmutableListMultimap.of(3, "one", 3, "two", 3, "one", 4, "five", 4, "five");
+ ImmutableListMultimap<Integer, String> expected =
+ ImmutableListMultimap.of(3, "two", 4, "five", 3, "one", 4, "five", 3, "one");
+
+ assertThat(actual).containsAtLeastEntriesIn(expected);
+ }
+
+ @Test
+ public void containsAtLeastRespectsDuplicatesFailure() {
+ ImmutableListMultimap<Integer, String> expected =
+ ImmutableListMultimap.of(3, "one", 3, "two", 3, "one", 4, "five", 4, "five");
+ ImmutableSetMultimap<Integer, String> actual = ImmutableSetMultimap.copyOf(expected);
+
+ expectFailureWhenTestingThat(actual).containsAtLeastEntriesIn(expected);
+ assertFailureKeys("missing", "---", "expected to contain at least", "but was");
+ assertFailureValue("missing", "{3=[one], 4=[five]}");
+ assertFailureValue("expected to contain at least", "{3=[one, two, one], 4=[five, five]}");
+ assertFailureValue("but was", "{3=[one, two], 4=[five]}");
+ }
+
+ @Test
+ public void containsAtLeastFailureMissing() {
+ ImmutableMultimap<Integer, String> expected =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+ ListMultimap<Integer, String> actual = LinkedListMultimap.create(expected);
+ actual.remove(3, "six");
+ actual.remove(4, "five");
+ actual.put(50, "hawaii");
+
+ expectFailureWhenTestingThat(actual).containsAtLeastEntriesIn(expected);
+ assertFailureKeys("missing", "---", "expected to contain at least", "but was");
+ assertFailureValue("missing", "{3=[six], 4=[five]}");
+ }
+
+ @Test
+ public void containsAtLeastFailureWithEmptyStringMissing() {
+ expectFailureWhenTestingThat(ImmutableMultimap.of("key", "value")).containsAtLeast("", "a");
+ assertFailureKeys("missing", "---", "expected to contain at least", "but was");
+ assertFailureValue("missing", "{\"\" (empty String)=[a]}");
+ }
+
+ @Test
+ public void containsAtLeastInOrder() {
+ ImmutableMultimap<Integer, String> actual =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+ ImmutableMultimap<Integer, String> expected =
+ ImmutableMultimap.of(3, "one", 3, "six", 4, "five", 4, "four");
+
+ assertThat(actual).containsAtLeastEntriesIn(expected).inOrder();
+ }
+
+ @Test
+ public void containsAtLeastInOrderDifferentTypes() {
+ ImmutableListMultimap<Integer, String> actual =
+ ImmutableListMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+ ImmutableSetMultimap<Integer, String> expected =
+ ImmutableSetMultimap.of(3, "one", 3, "six", 4, "five", 4, "four");
+
+ assertThat(actual).containsAtLeastEntriesIn(expected).inOrder();
+ }
+
+ @Test
+ public void containsAtLeastInOrderFailure() {
+ ImmutableMultimap<Integer, String> actual =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+ ImmutableMultimap<Integer, String> expected =
+ ImmutableMultimap.of(4, "four", 3, "six", 3, "two", 3, "one");
+
+ assertThat(actual).containsAtLeastEntriesIn(expected);
+ expectFailureWhenTestingThat(actual).containsAtLeastEntriesIn(expected).inOrder();
+ assertFailureKeys(
+ "contents match, but order was wrong",
+ "keys are not in order",
+ "keys with out-of-order values",
+ "---",
+ "expected to contain at least",
+ "but was");
+ assertFailureValue("keys with out-of-order values", "[3]");
+ assertFailureValue("expected to contain at least", "{4=[four], 3=[six, two, one]}");
+ assertFailureValue("but was", "{3=[one, six, two], 4=[five, four]}");
+ }
+
+ @Test
+ public void containsAtLeastInOrderFailureValuesOnly() {
+ ImmutableMultimap<Integer, String> actual =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+ ImmutableMultimap<Integer, String> expected =
+ ImmutableMultimap.of(3, "six", 3, "one", 4, "five", 4, "four");
+
+ assertThat(actual).containsAtLeastEntriesIn(expected);
+ expectFailureWhenTestingThat(actual).containsAtLeastEntriesIn(expected).inOrder();
+ assertFailureKeys(
+ "contents match, but order was wrong",
+ "keys with out-of-order values",
+ "---",
+ "expected to contain at least",
+ "but was");
+ assertFailureValue("keys with out-of-order values", "[3]");
+ }
+
+ @Test
+ public void containsAtLeastVararg() {
+ ImmutableListMultimap<Integer, String> listMultimap =
+ ImmutableListMultimap.of(1, "one", 3, "six", 3, "two", 3, "one");
+
+ assertThat(listMultimap).containsAtLeast(1, "one", 3, "six", 3, "two");
+ }
+
+ @Test
+ public void containsAtLeastVarargWithNull() {
+ Multimap<Integer, String> listMultimap =
+ LinkedListMultimap.create(ImmutableListMultimap.of(1, "one", 3, "six", 3, "two"));
+ listMultimap.put(4, null);
+
+ assertThat(listMultimap).containsAtLeast(1, "one", 3, "two", 4, null);
+ }
+
+ @Test
+ public void containsAtLeastVarargFailureMissing() {
+ ImmutableMultimap<Integer, String> expected =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+ ListMultimap<Integer, String> actual = LinkedListMultimap.create(expected);
+ actual.remove(3, "six");
+ actual.remove(4, "five");
+ actual.put(3, "nine");
+
+ expectFailureWhenTestingThat(actual)
+ .containsAtLeast(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+ assertFailureKeys("missing", "---", "expected to contain at least", "but was");
+ assertFailureValue("missing", "{3=[six], 4=[five]}");
+ }
+
+ @Test
+ public void containsAtLeastVarargRespectsDuplicates() {
+ ImmutableListMultimap<Integer, String> actual =
+ ImmutableListMultimap.of(3, "one", 3, "two", 3, "one", 4, "five", 4, "five");
+
+ assertThat(actual).containsAtLeast(3, "two", 4, "five", 3, "one", 3, "one");
+ }
+
+ @Test
+ public void containsAtLeastVarargRespectsDuplicatesFailure() {
+ ImmutableListMultimap<Integer, String> actual =
+ ImmutableListMultimap.of(3, "one", 3, "two", 4, "five", 4, "five");
+
+ expectFailureWhenTestingThat(actual).containsAtLeast(3, "one", 3, "one", 3, "one", 4, "five");
+ assertFailureKeys("missing", "---", "expected to contain at least", "but was");
+ assertFailureValue("missing", "{3=[one [2 copies]]}");
+ }
+
+ @Test
+ public void containsAtLeastVarargInOrder() {
+ ImmutableMultimap<Integer, String> actual =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+
+ assertThat(actual).containsAtLeast(3, "one", 3, "six", 4, "five", 4, "four").inOrder();
+ }
+
+ @Test
+ public void containsAtLeastVarargInOrderFailure() {
+ ImmutableMultimap<Integer, String> actual =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+
+ assertThat(actual).containsAtLeast(4, "four", 3, "six", 3, "two", 3, "one");
+ expectFailureWhenTestingThat(actual)
+ .containsAtLeast(4, "four", 3, "six", 3, "two", 3, "one")
+ .inOrder();
+ assertFailureKeys(
+ "contents match, but order was wrong",
+ "keys are not in order",
+ "keys with out-of-order values",
+ "---",
+ "expected to contain at least",
+ "but was");
+ assertFailureValue("keys with out-of-order values", "[3]");
+ assertFailureValue("expected to contain at least", "{4=[four], 3=[six, two, one]}");
+ assertFailureValue("but was", "{3=[one, six, two], 4=[five, four]}");
+ }
+
+ @Test
+ public void containsAtLeastVarargInOrderFailureValuesOnly() {
+ ImmutableMultimap<Integer, String> actual =
+ ImmutableMultimap.of(3, "one", 3, "six", 3, "two", 4, "five", 4, "four");
+
+ assertThat(actual).containsAtLeast(3, "two", 3, "one", 4, "five", 4, "four");
+ expectFailureWhenTestingThat(actual)
+ .containsAtLeast(3, "two", 3, "one", 4, "five", 4, "four")
+ .inOrder();
+ assertFailureKeys(
+ "contents match, but order was wrong",
+ "keys with out-of-order values",
+ "---",
+ "expected to contain at least",
+ "but was");
+ assertFailureValue("keys with out-of-order values", "[3]");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsEntry_success() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+456", "def", "+789");
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsEntry("def", 789);
+ }
+
+ @Test
+ public void comparingValuesUsing_containsEntry_failsExpectedKeyHasWrongValues() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+456", "def", "+789");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsEntry("def", 123);
+ assertFailureKeys(
+ "expected to contain entry",
+ "testing whether",
+ "but did not",
+ "though it did contain values for that key",
+ "full contents");
+ assertFailureValue("expected to contain entry", "def=123");
+ assertFailureValue("testing whether", "actual value parses to expected value");
+ assertFailureValue("though it did contain values for that key", "[+456, +789]");
+ assertFailureValue("full contents", "{abc=[+123], def=[+456, +789]}");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsEntry_failsWrongKeyHasExpectedValue() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+456", "def", "+789");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsEntry("xyz", 789);
+ assertFailureKeys(
+ "expected to contain entry",
+ "testing whether",
+ "but did not",
+ "though it did contain entries with matching values",
+ "full contents");
+ assertFailureValue("though it did contain entries with matching values", "[def=+789]");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsEntry_failsMissingExpectedKeyAndValue() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+456", "def", "+789");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsEntry("xyz", 321);
+ assertFailureKeys(
+ "expected to contain entry", "testing whether", "but did not", "full contents");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsEntry_handlesException_expectedKeyHasWrongValues() {
+ ListMultimap<Integer, String> actual = LinkedListMultimap.create();
+ actual.put(1, "one");
+ actual.put(2, "two");
+ actual.put(2, "deux");
+ actual.put(2, null);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(CASE_INSENSITIVE_EQUALITY)
+ .containsEntry(2, "ZWEI");
+ // The test fails because the expected key doesn't have a match for the expected value. We are
+ // bound also to hit a NPE from compare(null, ZWEI) along the way, and should also report that.
+ assertFailureKeys(
+ "expected to contain entry",
+ "testing whether",
+ "but did not",
+ "though it did contain values for that key",
+ "full contents",
+ "additionally, one or more exceptions were thrown while comparing values",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, ZWEI) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsEntry_handlesException_wrongKeyHasExpectedValue() {
+ ListMultimap<Integer, String> actual = LinkedListMultimap.create();
+ actual.put(1, "one");
+ actual.put(3, "two");
+ actual.put(3, null);
+ actual.put(3, "zwei");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(CASE_INSENSITIVE_EQUALITY)
+ .containsEntry(2, "ZWEI");
+ // The test fails and does not contain the expected key, but does contain the expected value
+ // we the wrong key. We are bound also to hit a NPE from compare(null, ZWEI) along the way, and
+ // should also report that.
+ assertFailureKeys(
+ "expected to contain entry",
+ "testing whether",
+ "but did not",
+ "though it did contain entries with matching values",
+ "full contents",
+ "additionally, one or more exceptions were thrown while comparing values",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, ZWEI) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsEntry_handlesException_alwaysFails() {
+ ListMultimap<Integer, String> actual = LinkedListMultimap.create();
+ actual.put(1, "one");
+ actual.put(2, "two");
+ actual.put(2, null);
+ actual.put(2, "zwei");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(CASE_INSENSITIVE_EQUALITY)
+ .containsEntry(2, "ZWEI");
+ // The multimap does contain the expected entry, but no reasonable implementation could find
+ // it without hitting the NPE from compare(null, ZWEI) first, so we are contractually required
+ // to fail.
+ assertFailureKeys(
+ "one or more exceptions were thrown while comparing values",
+ "first exception",
+ "expected to contain entry",
+ "testing whether",
+ "found match (but failing because of exception)",
+ "full contents");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, ZWEI) threw java.lang.NullPointerException");
+ assertFailureValue("found match (but failing because of exception)", "2=zwei");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsEntry_wrongTypeInActual() {
+ ImmutableListMultimap<String, Object> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+456", "def", 789);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsEntry("def", 789);
+ assertFailureKeys(
+ "expected to contain entry",
+ "testing whether",
+ "but did not",
+ "though it did contain values for that key",
+ "full contents",
+ "additionally, one or more exceptions were thrown while comparing values",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(789, 789) threw java.lang.ClassCastException");
+ }
+
+ @Test
+ public void comparingValuesUsing_doesNotContainEntry_successExcludeKeyHasWrongValues() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+456", "def", "+789");
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .doesNotContainEntry("def", 123);
+ }
+
+ @Test
+ public void comparingValuesUsing_doesNotContainEntry_successWrongKeyHasExcludedValue() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+456", "def", "+789");
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .doesNotContainEntry("xyz", 789);
+ }
+
+ @Test
+ public void comparingValuesUsing_doesNotContainEntry_successMissingExcludedKeyAndValue() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+456", "def", "+789");
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .doesNotContainEntry("xyz", 321);
+ }
+
+ @Test
+ public void comparingValuesUsing_doesNotContainEntry_failure() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+456", "def", "+789");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .doesNotContainEntry("def", 789);
+ assertFailureKeys(
+ "expected not to contain entry",
+ "testing whether",
+ "but contained that key with matching values",
+ "full contents");
+ assertFailureValue("expected not to contain entry", "def=789");
+ assertFailureValue("testing whether", "actual value parses to expected value");
+ assertFailureValue("but contained that key with matching values", "[+789]");
+ assertFailureValue("full contents", "{abc=[+123], def=[+456, +789]}");
+ }
+
+ @Test
+ public void comparingValuesUsing_doesNotContainEntry_handlesException_didContainEntry() {
+ ListMultimap<Integer, String> actual = LinkedListMultimap.create();
+ actual.put(1, "one");
+ actual.put(2, "two");
+ actual.put(2, null);
+ actual.put(2, "zwei");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(CASE_INSENSITIVE_EQUALITY)
+ .doesNotContainEntry(2, "ZWEI");
+ // The test fails because it does contain the expected entry. We are bound to also hit the NPE
+ // from compare(null, ZWEI) along the way, and should also report that.
+ assertFailureKeys(
+ "expected not to contain entry",
+ "testing whether",
+ "but contained that key with matching values",
+ "full contents",
+ "additionally, one or more exceptions were thrown while comparing values",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, ZWEI) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void comparingValuesUsing_doesNotContainEntry_handlesException_didNotContainEntry() {
+ ListMultimap<Integer, String> actual = LinkedListMultimap.create();
+ actual.put(1, "one");
+ actual.put(2, "two");
+ actual.put(2, "deux");
+ actual.put(2, null);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(CASE_INSENSITIVE_EQUALITY)
+ .doesNotContainEntry(2, "ZWEI");
+ // The test would pass if compare(null, ZWEI) returned false. But it actually throws NPE, and
+ // we are bound to hit that, so we are contractually required to fail.
+ assertFailureKeys(
+ "one or more exceptions were thrown while comparing values",
+ "first exception",
+ "expected not to contain entry",
+ "testing whether",
+ "found no match (but failing because of exception)",
+ "full contents");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(null, ZWEI) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void comparingValuesUsing_doesNotContainEntry_wrongTypeInActual() {
+ ImmutableListMultimap<String, Object> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+456", "def", 789);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .doesNotContainEntry("def", 789);
+ assertFailureKeys(
+ "one or more exceptions were thrown while comparing values",
+ "first exception",
+ "expected not to contain entry",
+ "testing whether",
+ "found no match (but failing because of exception)",
+ "full contents");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(789, 789) threw java.lang.ClassCastException");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactlyEntriesIn_success() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+64", "def", "0x40", "def", "+128");
+ ImmutableListMultimap<String, Integer> expected =
+ ImmutableListMultimap.of("def", 64, "def", 128, "def", 64, "abc", 123);
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyEntriesIn(expected);
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactlyEntriesIn_missingKey() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("def", "+64", "def", "0x40", "def", "+128");
+ ImmutableListMultimap<String, Integer> expected =
+ ImmutableListMultimap.of("def", 64, "def", 128, "def", 64, "abc", 123);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyEntriesIn(expected);
+ assertFailureKeys("missing (1)", "---", "expected", "testing whether", "but was");
+ assertFailureValue("missing (1)", "abc=123");
+ // TODO(b/69154276): Address the fact that we show "expected" as a list of entries and "but was"
+ // as a multimap, which looks a bit odd.
+ assertFailureValue("expected", "[def=64, def=128, def=64, abc=123]");
+ assertFailureValue(
+ "testing whether",
+ "actual element has a key that is equal to and a value that parses to the key and value of"
+ + " expected element");
+ assertFailureValue("but was", "{def=[+64, 0x40, +128]}");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactlyEntriesIn_extraKey() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+64", "def", "0x40", "def", "+128");
+ ImmutableListMultimap<String, Integer> expected =
+ ImmutableListMultimap.of("def", 64, "def", 128, "def", 64);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyEntriesIn(expected);
+ assertFailureKeys("unexpected (1)", "---", "expected", "testing whether", "but was");
+ assertFailureValue("unexpected (1)", "abc=+123");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactlyEntriesIn_wrongValueForKey() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+64", "def", "0x40", "def", "+128");
+ ImmutableListMultimap<String, Integer> expected =
+ ImmutableListMultimap.of("def", 64, "def", 128, "def", 128, "abc", 123);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyEntriesIn(expected);
+ assertFailureKeys(
+ "in an assertion requiring a 1:1 mapping between the expected and the actual elements,"
+ + " each actual element matches as least one expected element, and vice versa, but"
+ + " there was no 1:1 mapping",
+ "using the most complete 1:1 mapping (or one such mapping, if there is a tie)",
+ "missing (1)",
+ "unexpected (1)",
+ "---",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValue("missing (1)", "def=128");
+ assertThatFailure().factValue("unexpected (1)").isAnyOf("[def=+64]", "[def=0x40]");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactlyEntriesIn_handlesException() {
+ ListMultimap<Integer, String> actual = LinkedListMultimap.create();
+ actual.put(1, "one");
+ actual.put(2, null);
+ actual.put(2, "deux");
+ actual.put(2, "zwei");
+ ImmutableListMultimap<Integer, String> expected =
+ ImmutableListMultimap.of(1, "ONE", 2, "TWO", 2, "DEUX", 2, "ZWEI");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(CASE_INSENSITIVE_EQUALITY)
+ .containsExactlyEntriesIn(expected);
+ assertFailureKeys(
+ "missing (1)",
+ "unexpected (1)",
+ "---",
+ "expected",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertFailureValue("missing (1)", "2=TWO");
+ assertFailureValue("unexpected (1)", "[2=null]");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(2=null, 2=TWO) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactlyEntriesIn_handlesException_alwaysFails() {
+ ListMultimap<Integer, String> actual = LinkedListMultimap.create();
+ actual.put(1, "one");
+ actual.put(2, null);
+ actual.put(2, "two");
+ actual.put(2, "deux");
+ ListMultimap<Integer, String> expected = LinkedListMultimap.create();
+ expected.put(1, "ONE");
+ expected.put(2, "TWO");
+ expected.put(2, "DEUX");
+ expected.put(2, null);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(CASE_INSENSITIVE_EQUALITY_HALF_NULL_SAFE)
+ .containsExactlyEntriesIn(expected);
+ // CASE_INSENSITIVE_EQUALITY_HALF_NULL_SAFE.compare(null, null) returns true, so there is a
+ // mapping between actual and expected entries where they all correspond. However, no
+ // reasonable implementation would find that mapping without hitting the (null, "TWO") case
+ // along the way, and that throws NPE, so we are contractually required to fail.
+ assertFailureKeys(
+ "one or more exceptions were thrown while comparing elements",
+ "first exception",
+ "expected",
+ "testing whether",
+ "found all expected elements (but failing because of exception)",
+ "full contents");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(2=null, 2=TWO) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactlyEntriesIn_wrongTypeInActual() {
+ ImmutableListMultimap<String, Object> actual =
+ ImmutableListMultimap.<String, Object>of(
+ "abc", "+123", "def", "+64", "def", "0x40", "def", 999);
+ ImmutableListMultimap<String, Integer> expected =
+ ImmutableListMultimap.of("def", 64, "def", 123, "def", 64, "abc", 123);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyEntriesIn(expected);
+ assertFailureKeys(
+ "missing (1)",
+ "unexpected (1)",
+ "---",
+ "expected",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertFailureValue("missing (1)", "def=123");
+ assertFailureValue("unexpected (1)", "[def=999]");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(def=999, def=64) threw java.lang.ClassCastException");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactlyEntriesIn_inOrder_success() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+64", "def", "0x40", "def", "+128");
+ ImmutableListMultimap<String, Integer> expected =
+ ImmutableListMultimap.of("abc", 123, "def", 64, "def", 64, "def", 128);
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyEntriesIn(expected);
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactlyEntriesIn_inOrder_wrongKeyOrder() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+64", "def", "0x40", "def", "+128");
+ ImmutableListMultimap<String, Integer> expected =
+ ImmutableListMultimap.of("def", 64, "def", 64, "def", 128, "abc", 123);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyEntriesIn(expected)
+ .inOrder();
+ assertFailureKeys(
+ "contents match, but order was wrong", "expected", "testing whether", "but was");
+ assertFailureValue("expected", "[def=64, def=64, def=128, abc=123]");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactlyEntriesIn_inOrder_wrongValueOrder() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+64", "def", "0x40", "def", "+128");
+ ImmutableListMultimap<String, Integer> expected =
+ ImmutableListMultimap.of("abc", 123, "def", 64, "def", 128, "def", 64);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactlyEntriesIn(expected)
+ .inOrder();
+ assertFailureKeys(
+ "contents match, but order was wrong", "expected", "testing whether", "but was");
+ assertFailureValue("expected", "[abc=123, def=64, def=128, def=64]");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactlyNoArgs() {
+ ImmutableListMultimap<String, String> actual = ImmutableListMultimap.of();
+
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly();
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly()
+ .inOrder();
+
+ expectFailureWhenTestingThat(ImmutableListMultimap.of("abc", "+123"))
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly();
+ assertFailureKeys("expected to be empty", "but was");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactly_success() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+64", "def", "0x40", "def", "+128");
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly("def", 64, "def", 128, "def", 64, "abc", 123);
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactly_missingKey() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("def", "+64", "def", "0x40", "def", "+128");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly("def", 64, "def", 128, "def", 64, "abc", 123);
+ assertFailureKeys("missing (1)", "---", "expected", "testing whether", "but was");
+ assertFailureValue("missing (1)", "abc=123");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactly_extraKey() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+64", "def", "0x40", "def", "+128");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly("def", 64, "def", 128, "def", 64);
+ assertFailureKeys("unexpected (1)", "---", "expected", "testing whether", "but was");
+ assertFailureValue("unexpected (1)", "abc=+123");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactly_wrongValueForKey() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+64", "def", "0x40", "def", "+128");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly("def", 64, "def", 128, "def", 128, "abc", 123);
+ assertFailureKeys(
+ "in an assertion requiring a 1:1 mapping between the expected and the actual elements,"
+ + " each actual element matches as least one expected element, and vice versa, but"
+ + " there was no 1:1 mapping",
+ "using the most complete 1:1 mapping (or one such mapping, if there is a tie)",
+ "missing (1)",
+ "unexpected (1)",
+ "---",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValue("missing (1)", "def=128");
+ assertThatFailure().factValue("unexpected (1)").isAnyOf("[def=+64]", "[def=0x40]");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactly_wrongTypeInActual() {
+ ImmutableListMultimap<String, Object> actual =
+ ImmutableListMultimap.<String, Object>of(
+ "abc", "+123", "def", "+64", "def", "0x40", "def", 999);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly("def", 64, "def", 123, "def", 64, "abc", 123);
+ assertFailureKeys(
+ "missing (1)",
+ "unexpected (1)",
+ "---",
+ "expected",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertFailureValue("missing (1)", "def=123");
+ assertFailureValue("unexpected (1)", "[def=999]");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(def=999, def=64) threw java.lang.ClassCastException");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactly_nullKey() {
+ ListMultimap<String, String> actual = ArrayListMultimap.create();
+ actual.put(null, "+123");
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly(null, 123);
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactly_inOrder_success() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+64", "def", "0x40", "def", "+128");
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly("abc", 123, "def", 64, "def", 64, "def", 128);
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactly_inOrder_wrongKeyOrder() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+64", "def", "0x40", "def", "+128");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly("def", 64, "def", 64, "def", 128, "abc", 123)
+ .inOrder();
+ assertFailureKeys(
+ "contents match, but order was wrong", "expected", "testing whether", "but was");
+ assertFailureValue("expected", "[def=64, def=64, def=128, abc=123]");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsExactly_inOrder_wrongValueOrder() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+64", "def", "0x40", "def", "+128");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsExactly("abc", 123, "def", 64, "def", 128, "def", 64)
+ .inOrder();
+ assertFailureKeys(
+ "contents match, but order was wrong", "expected", "testing whether", "but was");
+ assertFailureValue("expected", "[abc=123, def=64, def=128, def=64]");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeastEntriesIn_success() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+64", "def", "0x40", "def", "+128");
+ ImmutableListMultimap<String, Integer> expected =
+ ImmutableListMultimap.of("def", 64, "def", 128, "abc", 123);
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastEntriesIn(expected);
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeastEntriesIn_missingKey() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("def", "+64", "def", "0x40", "def", "+128", "abc", "+99");
+ ImmutableListMultimap<String, Integer> expected =
+ ImmutableListMultimap.of("def", 64, "def", 128, "def", 64, "abc", 123);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastEntriesIn(expected);
+ assertFailureKeys(
+ "missing (1)", "---", "expected to contain at least", "testing whether", "but was");
+ assertFailureValue("missing (1)", "abc=123");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeastEntriesIn_wrongValueForKey() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("abc", "+123", "def", "+64", "def", "0x40", "def", "+128");
+ ImmutableListMultimap<String, Integer> expected =
+ ImmutableListMultimap.of("def", 64, "def", 128, "def", 128, "abc", 123);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastEntriesIn(expected);
+ assertFailureKeys(
+ "in an assertion requiring a 1:1 mapping between the expected and a subset of the actual"
+ + " elements, each actual element matches as least one expected element, and vice"
+ + " versa, but there was no 1:1 mapping",
+ "using the most complete 1:1 mapping (or one such mapping, if there is a tie)",
+ "missing (1)",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was");
+ assertFailureValue("missing (1)", "def=128");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeastEntriesIn_handlesException() {
+ ListMultimap<Integer, String> actual = LinkedListMultimap.create();
+ actual.put(1, "one");
+ actual.put(2, null);
+ actual.put(2, "deux");
+ actual.put(2, "zwei");
+ ImmutableListMultimap<Integer, String> expected =
+ ImmutableListMultimap.of(1, "ONE", 2, "TWO", 2, "DEUX");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(CASE_INSENSITIVE_EQUALITY)
+ .containsAtLeastEntriesIn(expected);
+ assertFailureKeys(
+ "missing (1)",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertFailureValue("missing (1)", "2=TWO");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(2=null, 2=TWO) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeastEntriesIn_handlesException_alwaysFails() {
+ ListMultimap<Integer, String> actual = LinkedListMultimap.create();
+ actual.put(1, "one");
+ actual.put(2, null);
+ actual.put(2, "two");
+ actual.put(2, "deux");
+ ListMultimap<Integer, String> expected = LinkedListMultimap.create();
+ expected.put(1, "ONE");
+ expected.put(2, "TWO");
+ expected.put(2, "DEUX");
+ expected.put(2, null);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(CASE_INSENSITIVE_EQUALITY_HALF_NULL_SAFE)
+ .containsAtLeastEntriesIn(expected);
+ // CASE_INSENSITIVE_EQUALITY_HALF_NULL_SAFE.compare(null, null) returns true, so there is a
+ // mapping between actual and expected entries where they all correspond. However, no
+ // reasonable implementation would find that mapping without hitting the (null, "TWO") case
+ // along the way, and that throws NPE, so we are contractually required to fail.
+ assertFailureKeys(
+ "one or more exceptions were thrown while comparing elements",
+ "first exception",
+ "expected to contain at least",
+ "testing whether",
+ "found all expected elements (but failing because of exception)",
+ "full contents");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(2=null, 2=TWO) threw java.lang.NullPointerException");
+ assertFailureValue("expected to contain at least", "[1=ONE, 2=TWO, 2=DEUX, 2=null]");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeastEntriesIn_wrongTypeInActual() {
+ ImmutableListMultimap<String, Object> actual =
+ ImmutableListMultimap.<String, Object>of(
+ "abc", "+123", "def", "+64", "def", "0x40", "def", 999);
+ ImmutableListMultimap<String, Integer> expected =
+ ImmutableListMultimap.of("def", 64, "def", 123, "def", 64, "abc", 123);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastEntriesIn(expected);
+ assertFailureKeys(
+ "missing (1)",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertFailureValue("missing (1)", "def=123");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(def=999, def=64) threw java.lang.ClassCastException");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeastEntriesIn_inOrder_success() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of(
+ "def", "+64", "abc", "+123", "def", "0x40", "m", "+1", "def", "+128");
+ ImmutableListMultimap<String, Integer> expected =
+ ImmutableListMultimap.of("def", 64, "def", 64, "def", 128, "abc", 123);
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastEntriesIn(expected);
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeastEntriesIn_inOrder_wrongKeyOrder() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of(
+ "abc", "+123", "def", "+64", "m", "+1", "def", "0x40", "def", "+128");
+ ImmutableListMultimap<String, Integer> expected =
+ ImmutableListMultimap.of("def", 64, "def", 64, "def", 128, "abc", 123);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastEntriesIn(expected)
+ .inOrder();
+ assertFailureKeys(
+ "required elements were all found, but order was wrong",
+ "expected order for required elements",
+ "testing whether",
+ "but was");
+ assertFailureValue(
+ "expected order for required elements", "[def=64, def=64, def=128, abc=123]");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeastEntriesIn_inOrder_wrongValueOrder() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of(
+ "abc", "+123", "def", "+64", "m", "+1", "def", "0x40", "def", "+128");
+ ImmutableListMultimap<String, Integer> expected =
+ ImmutableListMultimap.of("abc", 123, "def", 64, "def", 128, "def", 64);
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeastEntriesIn(expected)
+ .inOrder();
+ assertFailureKeys(
+ "required elements were all found, but order was wrong",
+ "expected order for required elements",
+ "testing whether",
+ "but was");
+ assertFailureValue(
+ "expected order for required elements", "[abc=123, def=64, def=128, def=64]");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeast_success() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of(
+ "abc", "+123", "def", "+64", "m", "+1", "def", "0x40", "def", "+128");
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeast("def", 64, "def", 128, "def", 64, "abc", 123);
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeast_missingKey() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of("def", "+64", "def", "0x40", "m", "+1", "def", "+128");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeast("def", 64, "def", 128, "def", 64, "abc", 123);
+ assertFailureKeys(
+ "missing (1)", "---", "expected to contain at least", "testing whether", "but was");
+ assertFailureValue("missing (1)", "abc=123");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeast_wrongValueForKey() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of(
+ "abc", "+123", "def", "+64", "m", "+1", "def", "0x40", "def", "+128");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeast("def", 64, "def", 128, "def", 128, "abc", 123);
+ assertFailureKeys(
+ "in an assertion requiring a 1:1 mapping between the expected and a subset of the actual"
+ + " elements, each actual element matches as least one expected element, and vice"
+ + " versa, but there was no 1:1 mapping",
+ "using the most complete 1:1 mapping (or one such mapping, if there is a tie)",
+ "missing (1)",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was");
+ assertFailureValue("missing (1)", "def=128");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeast_wrongTypeInActual() {
+ ImmutableListMultimap<String, Object> actual =
+ ImmutableListMultimap.<String, Object>of(
+ "abc", "+123", "def", "+64", "def", "0x40", "def", 999, "m", "+1");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeast("def", 64, "def", 123, "def", 64, "abc", 123);
+ assertFailureKeys(
+ "missing (1)",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertFailureValue("missing (1)", "def=123");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(def=999, def=64) threw java.lang.ClassCastException");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeast_inOrder_success() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of(
+ "abc", "+123", "def", "+64", "m", "+1", "def", "0x40", "def", "+128");
+ assertThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeast("abc", 123, "def", 64, "def", 64, "def", 128);
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeast_inOrder_wrongKeyOrder() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of(
+ "abc", "+123", "def", "+64", "def", "0x40", "m", "+1", "def", "+128");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeast("def", 64, "def", 64, "def", 128, "abc", 123)
+ .inOrder();
+ assertFailureKeys(
+ "required elements were all found, but order was wrong",
+ "expected order for required elements",
+ "testing whether",
+ "but was");
+ assertFailureValue(
+ "expected order for required elements", "[def=64, def=64, def=128, abc=123]");
+ }
+
+ @Test
+ public void comparingValuesUsing_containsAtLeast_inOrder_wrongValueOrder() {
+ ImmutableListMultimap<String, String> actual =
+ ImmutableListMultimap.of(
+ "abc", "+123", "m", "+1", "def", "+64", "def", "0x40", "def", "+128");
+ expectFailureWhenTestingThat(actual)
+ .comparingValuesUsing(STRING_PARSES_TO_INTEGER_CORRESPONDENCE)
+ .containsAtLeast("abc", 123, "def", 64, "def", 128, "def", 64)
+ .inOrder();
+ assertFailureKeys(
+ "required elements were all found, but order was wrong",
+ "expected order for required elements",
+ "testing whether",
+ "but was");
+ assertFailureValue(
+ "expected order for required elements", "[abc=123, def=64, def=128, def=64]");
+ }
+
+ private MultimapSubject expectFailureWhenTestingThat(Multimap<?, ?> actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/MultisetSubjectTest.java b/core/src/test/java/com/google/common/truth/MultisetSubjectTest.java
new file mode 100644
index 00000000..b8dd0f93
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/MultisetSubjectTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import com.google.common.collect.ImmutableMultiset;
+import com.google.common.collect.Multiset;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Multiset Subjects.
+ *
+ * @author Kurt Alfred Kluever
+ */
+@RunWith(JUnit4.class)
+public class MultisetSubjectTest extends BaseSubjectTestCase {
+
+ @Test
+ public void hasCount() {
+ ImmutableMultiset<String> multiset = ImmutableMultiset.of("kurt", "kurt", "kluever");
+ assertThat(multiset).hasCount("kurt", 2);
+ assertThat(multiset).hasCount("kluever", 1);
+ assertThat(multiset).hasCount("alfred", 0);
+
+ assertWithMessage("name").that(multiset).hasCount("kurt", 2);
+ }
+
+ @Test
+ public void hasCountFail() {
+ ImmutableMultiset<String> multiset = ImmutableMultiset.of("kurt", "kurt", "kluever");
+ expectFailureWhenTestingThat(multiset).hasCount("kurt", 3);
+ assertFailureValue("value of", "multiset.count(kurt)");
+ }
+
+ private MultisetSubject expectFailureWhenTestingThat(Multiset<?> actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/NoJUnitTest.java b/core/src/test/java/com/google/common/truth/NoJUnitTest.java
new file mode 100644
index 00000000..45e10bdf
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/NoJUnitTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2021 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+
+/** Truth-using binary to be run without JUnit on the classpath to verify that it still works. */
+public final class NoJUnitTest {
+ public static void main(String[] args) {
+ try {
+ assertThat("a").isEqualTo("b");
+ throw new Error("assertion should have failed");
+ } catch (AssertionError expected) {
+ ImmutableList<Fact> facts = ((AssertionErrorWithFacts) expected).facts();
+ assertThat(facts.get(0).key).isEqualTo("expected");
+ assertThat(facts.get(0).value).isEqualTo("b");
+ assertThat(facts.get(1).key).isEqualTo("but was");
+ assertThat(facts.get(1).value).isEqualTo("a");
+ }
+ }
+
+ private NoJUnitTest() {}
+}
diff --git a/core/src/test/java/com/google/common/truth/ObjectArraySubjectTest.java b/core/src/test/java/com/google/common/truth/ObjectArraySubjectTest.java
new file mode 100644
index 00000000..d33682a8
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/ObjectArraySubjectTest.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link com.google.common.truth.ObjectArraySubject}.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+@RunWith(JUnit4.class)
+public class ObjectArraySubjectTest extends BaseSubjectTestCase {
+ private static final Object[] EMPTY = new Object[0];
+
+ @Test
+ public void isEqualTo() {
+ assertThat(objectArray("A", 5L)).isEqualTo(objectArray("A", 5L));
+ }
+
+ @SuppressWarnings("TruthSelfEquals")
+ @Test
+ public void isEqualTo_Same() {
+ Object[] same = objectArray("A", 5L);
+ assertThat(same).isEqualTo(same);
+ }
+
+ @Test
+ public void asList() {
+ assertThat(objectArray("A", 5L)).asList().contains("A");
+ }
+
+ @Test
+ public void hasLength() {
+ assertThat(EMPTY).hasLength(0);
+ assertThat(objectArray("A", 5L)).hasLength(2);
+ assertThat(new Object[][] {}).hasLength(0);
+ assertThat(new Object[][] {{}}).hasLength(1);
+ }
+
+ @Test
+ public void hasLengthFail() {
+ expectFailureWhenTestingThat(objectArray("A", 5L)).hasLength(1);
+ assertFailureValue("value of", "array.length");
+ }
+
+ @Test
+ public void hasLengthMultiFail() {
+ expectFailureWhenTestingThat(new Object[][] {{"A"}, {5L}}).hasLength(1);
+ assertFailureValue("value of", "array.length");
+ }
+
+ @Test
+ public void hasLengthNegative() {
+ try {
+ assertThat(objectArray(2, 5)).hasLength(-1);
+ fail("Should have failed");
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test
+ public void isEmpty() {
+ assertThat(EMPTY).isEmpty();
+ assertThat(new Object[][] {}).isEmpty();
+ }
+
+ @Test
+ public void isEmptyFail() {
+ expectFailureWhenTestingThat(objectArray("A", 5L)).isEmpty();
+ assertFailureKeys("expected to be empty", "but was");
+ }
+
+ @Test
+ public void isNotEmpty() {
+ assertThat(objectArray("A", 5L)).isNotEmpty();
+ assertThat(new Object[][] {{"A"}, {5L}}).isNotEmpty();
+ }
+
+ @Test
+ public void isNotEmptyFail() {
+ expectFailureWhenTestingThat(EMPTY).isNotEmpty();
+ assertFailureKeys("expected not to be empty");
+ }
+
+ @Test
+ public void isEqualTo_Fail_UnequalOrdering() {
+ expectFailureWhenTestingThat(objectArray("A", 5L)).isEqualTo(objectArray(5L, "A"));
+ assertFailureValue("differs at index", "[0]");
+ }
+
+ @Test
+ public void isEqualTo_Fail_UnequalOrderingMultiDimensional_00() {
+ expectFailureWhenTestingThat(new Object[][] {{"A"}, {5L}})
+ .isEqualTo(new Object[][] {{5L}, {"A"}});
+ assertFailureValue("differs at index", "[0][0]");
+ }
+
+ @Test
+ public void isEqualTo_Fail_UnequalOrderingMultiDimensional_01() {
+ expectFailureWhenTestingThat(new Object[][] {{"A", "B"}, {5L}})
+ .isEqualTo(new Object[][] {{"A"}, {5L}});
+ assertFailureValue("wrong length for index", "[0]");
+ assertFailureValueIndexed("expected", 1, "1");
+ assertFailureValueIndexed("but was", 1, "2");
+ }
+
+ @Test
+ public void isEqualTo_Fail_UnequalOrderingMultiDimensional_11() {
+ expectFailureWhenTestingThat(new Object[][] {{"A"}, {5L}})
+ .isEqualTo(new Object[][] {{"A"}, {5L, 6L}});
+ assertFailureValue("wrong length for index", "[1]");
+ assertFailureValueIndexed("expected", 1, "2");
+ assertFailureValueIndexed("but was", 1, "1");
+ }
+
+ @Test
+ public void isEqualTo_Fail_NotAnArray() {
+ expectFailureWhenTestingThat(objectArray("A", 5L)).isEqualTo(new Object());
+ }
+
+ @Test
+ public void isNotEqualTo_SameLengths() {
+ assertThat(objectArray("A", 5L)).isNotEqualTo(objectArray("C", 5L));
+ assertThat(new Object[][] {{"A"}, {5L}}).isNotEqualTo(new Object[][] {{"C"}, {5L}});
+ }
+
+ @Test
+ public void isNotEqualTo_DifferentLengths() {
+ assertThat(objectArray("A", 5L)).isNotEqualTo(objectArray("A", 5L, "c"));
+ assertThat(new Object[][] {{"A"}, {5L}}).isNotEqualTo(new Object[][] {{"A", "c"}, {5L}});
+ assertThat(new Object[][] {{"A"}, {5L}}).isNotEqualTo(new Object[][] {{"A"}, {5L}, {"C"}});
+ }
+
+ @Test
+ public void isNotEqualTo_DifferentTypes() {
+ assertThat(objectArray("A", 5L)).isNotEqualTo(new Object());
+ }
+
+ @Test
+ public void isNotEqualTo_FailEquals() {
+ expectFailureWhenTestingThat(objectArray("A", 5L)).isNotEqualTo(objectArray("A", 5L));
+ }
+
+ @Test
+ public void isNotEqualTo_FailEqualsMultiDimensional() {
+ expectFailureWhenTestingThat(new Object[][] {{"A"}, {5L}})
+ .isNotEqualTo(new Object[][] {{"A"}, {5L}});
+ assertFailureValue("expected not to be", "[[A], [5]]");
+ }
+
+ @SuppressWarnings("TruthSelfEquals")
+ @Test
+ public void isNotEqualTo_FailSame() {
+ Object[] same = objectArray("A", 5L);
+ expectFailureWhenTestingThat(same).isNotEqualTo(same);
+ }
+
+ @SuppressWarnings("TruthSelfEquals")
+ @Test
+ public void isNotEqualTo_FailSameMultiDimensional() {
+ Object[][] same = new Object[][] {{"A"}, {5L}};
+ expectFailureWhenTestingThat(same).isNotEqualTo(same);
+ }
+
+ @Test
+ public void stringArrayIsEqualTo() {
+ assertThat(objectArray("A", "B")).isEqualTo(objectArray("A", "B"));
+ assertThat(new String[][] {{"A"}, {"B"}}).isEqualTo(new String[][] {{"A"}, {"B"}});
+ }
+
+ @Test
+ public void stringArrayAsList() {
+ assertThat(objectArray("A", "B")).asList().contains("A");
+ }
+
+ @Test
+ public void multiDimensionalStringArrayAsList() {
+ String[] ab = {"A", "B"};
+ assertThat(new String[][] {ab, {"C"}}).asList().contains(ab);
+ }
+
+ @Test
+ public void stringArrayIsEqualTo_Fail_UnequalLength() {
+ expectFailureWhenTestingThat(objectArray("A", "B")).isEqualTo(objectArray("B"));
+ assertFailureKeys("expected", "but was", "wrong length", "expected", "but was");
+ assertFailureValueIndexed("expected", 1, "1");
+ assertFailureValueIndexed("but was", 1, "2");
+ }
+
+ @Test
+ public void stringArrayIsEqualTo_Fail_UnequalLengthMultiDimensional() {
+ expectFailureWhenTestingThat(new String[][] {{"A"}, {"B"}}).isEqualTo(new String[][] {{"A"}});
+ assertFailureKeys("expected", "but was", "wrong length", "expected", "but was");
+ assertFailureValueIndexed("expected", 1, "1");
+ assertFailureValueIndexed("but was", 1, "2");
+ }
+
+ @Test
+ public void stringArrayIsEqualTo_Fail_UnequalOrdering() {
+ expectFailureWhenTestingThat(objectArray("A", "B")).isEqualTo(objectArray("B", "A"));
+ assertFailureValue("differs at index", "[0]");
+ }
+
+ @Test
+ public void stringArrayIsEqualTo_Fail_UnequalOrderingMultiDimensional() {
+ expectFailureWhenTestingThat(new String[][] {{"A"}, {"B"}})
+ .isEqualTo(new String[][] {{"B"}, {"A"}});
+ assertFailureValue("differs at index", "[0][0]");
+ }
+
+ @Test
+ public void setArrayIsEqualTo_Fail_UnequalOrdering() {
+ expectFailureWhenTestingThat(objectArray(ImmutableSet.of("A"), ImmutableSet.of("B")))
+ .isEqualTo(objectArray(ImmutableSet.of("B"), ImmutableSet.of("A")));
+ assertFailureValue("differs at index", "[0]");
+ // Maybe one day:
+ // .hasMessage("Not true that <(Set<String>[]) [[A], [B]]> is equal to <[[B], [A]]>");
+ }
+
+ @Test
+ public void primitiveMultiDimensionalArrayIsEqualTo() {
+ assertThat(new int[][] {{1, 2}, {3}, {4, 5, 6}})
+ .isEqualTo(new int[][] {{1, 2}, {3}, {4, 5, 6}});
+ }
+
+ @Test
+ public void primitiveMultiDimensionalArrayIsEqualTo_Fail_UnequalOrdering() {
+ expectFailureWhenTestingThat(new int[][] {{1, 2}, {3}, {4, 5, 6}})
+ .isEqualTo(new int[][] {{1, 2}, {3}, {4, 5, 6, 7}});
+ assertFailureValue("wrong length for index", "[2]");
+ assertFailureValueIndexed("expected", 1, "4");
+ assertFailureValueIndexed("but was", 1, "3");
+ }
+
+ @Test
+ public void primitiveMultiDimensionalArrayIsNotEqualTo() {
+ assertThat(new int[][] {{1, 2}, {3}, {4, 5, 6}})
+ .isNotEqualTo(new int[][] {{1, 2}, {3}, {4, 5, 6, 7}});
+ }
+
+ @Test
+ public void primitiveMultiDimensionalArrayIsNotEqualTo_Fail_Equal() {
+ expectFailureWhenTestingThat(new int[][] {{1, 2}, {3}, {4, 5, 6}})
+ .isNotEqualTo(new int[][] {{1, 2}, {3}, {4, 5, 6}});
+ }
+
+ @Test
+ public void boxedAndUnboxed() {
+ expectFailureWhenTestingThat(new Object[] {new int[] {0}})
+ .isEqualTo(new Object[] {new Integer[] {0}});
+ assertFailureValue("wrong type for index", "[0]");
+ assertFailureValueIndexed("expected", 1, "Object[]");
+ assertFailureValueIndexed("but was", 1, "int[]");
+ }
+
+ private static Object[] objectArray(Object... ts) {
+ return ts;
+ }
+
+ private static String[] objectArray(String... ts) {
+ return ts;
+ }
+
+ private static Set[] objectArray(Set... ts) {
+ return ts;
+ }
+
+ private ObjectArraySubject<?> expectFailureWhenTestingThat(Object[] actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/PlatformBaseSubjectTestCase.java b/core/src/test/java/com/google/common/truth/PlatformBaseSubjectTestCase.java
new file mode 100644
index 00000000..5b37e9b9
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/PlatformBaseSubjectTestCase.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2017 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import org.junit.Rule;
+
+/** Base class for truth subject tests to extend. */
+public abstract class PlatformBaseSubjectTestCase {
+ @Rule public final ExpectFailure expectFailure = new ExpectFailure();
+}
diff --git a/core/src/test/java/com/google/common/truth/PrimitiveBooleanArraySubjectTest.java b/core/src/test/java/com/google/common/truth/PrimitiveBooleanArraySubjectTest.java
new file mode 100644
index 00000000..b8e33df8
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/PrimitiveBooleanArraySubjectTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link com.google.common.truth.PrimitiveBooleanArraySubject}.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+@RunWith(JUnit4.class)
+public class PrimitiveBooleanArraySubjectTest extends BaseSubjectTestCase {
+
+ @Test
+ public void isEqualTo() {
+ assertThat(array(true, false, true)).isEqualTo(array(true, false, true));
+ }
+
+ @SuppressWarnings("TruthSelfEquals")
+ @Test
+ public void isEqualTo_Same() {
+ boolean[] same = array(true, false, true);
+ assertThat(same).isEqualTo(same);
+ }
+
+ @Test
+ public void asList() {
+ assertThat(array(true, true, false)).asList().containsAtLeast(true, false);
+ }
+
+ @Test
+ public void isEqualTo_Fail_UnequalOrdering() {
+ expectFailureWhenTestingThat(array(true, false, true)).isEqualTo(array(false, true, true));
+ assertFailureValue("differs at index", "[0]");
+ }
+
+ @Test
+ public void isEqualTo_Fail_NotAnArray() {
+ expectFailureWhenTestingThat(array(true, false, true)).isEqualTo(new Object());
+ }
+
+ @Test
+ public void isNotEqualTo_SameLengths() {
+ assertThat(array(true, false)).isNotEqualTo(array(true, true));
+ }
+
+ @Test
+ public void isNotEqualTo_DifferentLengths() {
+ assertThat(array(true, false)).isNotEqualTo(array(true, false, true));
+ }
+
+ @Test
+ public void isNotEqualTo_DifferentTypes() {
+ assertThat(array(true, false)).isNotEqualTo(new Object());
+ }
+
+ @Test
+ public void isNotEqualTo_FailEquals() {
+ expectFailureWhenTestingThat(array(true, false)).isNotEqualTo(array(true, false));
+ }
+
+ @SuppressWarnings("TruthSelfEquals")
+ @Test
+ public void isNotEqualTo_FailSame() {
+ boolean[] same = array(true, false);
+ expectFailureWhenTestingThat(same).isNotEqualTo(same);
+ }
+
+ private static boolean[] array(boolean... ts) {
+ return ts;
+ }
+
+ private PrimitiveBooleanArraySubject expectFailureWhenTestingThat(boolean[] actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/PrimitiveByteArraySubjectTest.java b/core/src/test/java/com/google/common/truth/PrimitiveByteArraySubjectTest.java
new file mode 100644
index 00000000..f0d4812c
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/PrimitiveByteArraySubjectTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link com.google.common.truth.PrimitiveByteArraySubject}.
+ *
+ * @author Kurt Alfred Kluever
+ */
+@RunWith(JUnit4.class)
+public class PrimitiveByteArraySubjectTest extends BaseSubjectTestCase {
+ private static final byte BYTE_0 = (byte) 0;
+ private static final byte BYTE_1 = (byte) 1;
+ private static final byte BYTE_2 = (byte) 2;
+
+ @Test
+ public void isEqualTo() {
+ assertThat(array(BYTE_0, BYTE_1)).isEqualTo(array(BYTE_0, BYTE_1));
+ }
+
+ @SuppressWarnings("TruthSelfEquals")
+ @Test
+ public void isEqualTo_Same() {
+ byte[] same = array(BYTE_0, BYTE_1);
+ assertThat(same).isEqualTo(same);
+ }
+
+ @Test
+ public void asList() {
+ assertThat(array(BYTE_0, BYTE_1, BYTE_2)).asList().containsAtLeast(BYTE_0, BYTE_2);
+ }
+
+ @Test
+ public void isEqualTo_Fail_UnequalOrdering() {
+ expectFailureWhenTestingThat(array(BYTE_0, (byte) 123)).isEqualTo(array((byte) 123, BYTE_0));
+ assertFailureKeys("expected", "but was", "expected", "but was");
+ assertFailureValueIndexed("expected", 0, "7B00");
+ assertFailureValueIndexed("but was", 0, "007B");
+ assertFailureValueIndexed("expected", 1, "[123, 0]");
+ assertFailureValueIndexed("but was", 1, "[0, 123]");
+ assertThat(expectFailure.getFailure()).isInstanceOf(ComparisonFailureWithFacts.class);
+ }
+
+ @Test
+ public void isEqualTo_Fail_NotAnArray() {
+ expectFailureWhenTestingThat(array(BYTE_0, BYTE_1)).isEqualTo(new int[] {});
+ assertFailureKeys("expected", "but was", "wrong type", "expected", "but was");
+ assertFailureValueIndexed("expected", 1, "int[]");
+ assertFailureValueIndexed("but was", 1, "byte[]");
+ }
+
+ @Test
+ public void isNotEqualTo_SameLengths() {
+ assertThat(array(BYTE_0, BYTE_1)).isNotEqualTo(array(BYTE_1, BYTE_0));
+ }
+
+ @Test
+ public void isNotEqualTo_DifferentLengths() {
+ assertThat(array(BYTE_0, BYTE_1)).isNotEqualTo(array(BYTE_1, BYTE_0, BYTE_2));
+ }
+
+ @Test
+ public void isNotEqualTo_DifferentTypes() {
+ assertThat(array(BYTE_0, BYTE_1)).isNotEqualTo(new Object());
+ }
+
+ @Test
+ public void isNotEqualTo_FailEquals() {
+ expectFailureWhenTestingThat(array(BYTE_0, BYTE_1)).isNotEqualTo(array(BYTE_0, BYTE_1));
+ }
+
+ @SuppressWarnings("TruthSelfEquals")
+ @Test
+ public void isNotEqualTo_FailSame() {
+ byte[] same = array(BYTE_0, BYTE_1);
+ expectFailureWhenTestingThat(same).isNotEqualTo(same);
+ }
+
+ private static byte[] array(byte... ts) {
+ return ts;
+ }
+
+ private PrimitiveByteArraySubject expectFailureWhenTestingThat(byte[] actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/PrimitiveCharArraySubjectTest.java b/core/src/test/java/com/google/common/truth/PrimitiveCharArraySubjectTest.java
new file mode 100644
index 00000000..ee3b4b60
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/PrimitiveCharArraySubjectTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link com.google.common.truth.PrimitiveCharArraySubject}.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+@RunWith(JUnit4.class)
+public class PrimitiveCharArraySubjectTest extends BaseSubjectTestCase {
+
+ @Test
+ public void isEqualTo() {
+ assertThat(array('a', 'q')).isEqualTo(array('a', 'q'));
+ }
+
+ @SuppressWarnings("TruthSelfEquals")
+ @Test
+ public void isEqualTo_Same() {
+ char[] same = array('a', 'q');
+ assertThat(same).isEqualTo(same);
+ }
+
+ @Test
+ public void asList() {
+ assertThat(array('a', 'q', 'z')).asList().containsAtLeast('a', 'z');
+ }
+
+ @Test
+ public void isEqualTo_Fail_UnequalOrdering() {
+ expectFailureWhenTestingThat(array('a', 'q')).isEqualTo(array('q', 'a'));
+ assertFailureKeys("expected", "but was", "differs at index");
+ assertFailureValue("expected", "[q, a]");
+ assertFailureValue("but was", "[a, q]");
+ assertFailureValue("differs at index", "[0]");
+ }
+
+ @Test
+ public void isEqualTo_Fail_DifferentKindOfArray() {
+ expectFailureWhenTestingThat(array('a', 'q')).isEqualTo(new int[] {});
+ assertFailureKeys("expected", "but was", "wrong type", "expected", "but was");
+ assertFailureValueIndexed("expected", 1, "int[]");
+ assertFailureValueIndexed("but was", 1, "char[]");
+ }
+
+ @Test
+ public void isNotEqualTo_SameLengths() {
+ assertThat(array('a', 'q')).isNotEqualTo(array('q', 'a'));
+ }
+
+ @Test
+ public void isNotEqualTo_DifferentLengths() {
+ assertThat(array('a', 'q')).isNotEqualTo(array('q', 'a', 'b'));
+ }
+
+ @Test
+ public void isNotEqualTo_DifferentTypes() {
+ assertThat(array('a', 'q')).isNotEqualTo(new Object());
+ }
+
+ @Test
+ public void isNotEqualTo_FailEquals() {
+ expectFailureWhenTestingThat(array('a', 'q')).isNotEqualTo(array('a', 'q'));
+ }
+
+ @SuppressWarnings("TruthSelfEquals")
+ @Test
+ public void isNotEqualTo_FailSame() {
+ char[] same = array('a', 'q');
+ expectFailureWhenTestingThat(same).isNotEqualTo(same);
+ }
+
+ private static char[] array(char... ts) {
+ return ts;
+ }
+
+ private PrimitiveCharArraySubject expectFailureWhenTestingThat(char[] actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/PrimitiveDoubleArraySubjectTest.java b/core/src/test/java/com/google/common/truth/PrimitiveDoubleArraySubjectTest.java
new file mode 100644
index 00000000..6e229f10
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/PrimitiveDoubleArraySubjectTest.java
@@ -0,0 +1,667 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.lang.Double.NEGATIVE_INFINITY;
+import static java.lang.Double.NaN;
+import static java.lang.Double.POSITIVE_INFINITY;
+import static java.lang.Math.nextAfter;
+import static org.junit.Assert.fail;
+
+import com.google.common.annotations.GwtIncompatible;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link PrimitiveDoubleArraySubject}.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+@RunWith(JUnit4.class)
+public class PrimitiveDoubleArraySubjectTest extends BaseSubjectTestCase {
+ private static final double DEFAULT_TOLERANCE = 0.000005d;
+
+ private static final double OVER_2POINT2 = 2.2000000000000006d;
+ private static final double TOLERABLE_2 = 2.0000049999999994d;
+ private static final double TOLERABLE_2POINT2 = 2.2000049999999995d;
+ private static final double INTOLERABLE_2POINT2 = 2.2000050000000004d;
+ private static final double TOLERABLE_3POINT3 = 3.300004999999999d;
+ private static final double INTOLERABLE_3POINT3 = 3.300005d;
+ private static final double UNDER_MIN_OF_LONG = -9.223372036854778E18d;
+
+ @Test
+ @GwtIncompatible("Math.nextAfter")
+ public void testDoubleConstants_matchNextAfter() {
+ assertThat(nextAfter(2.0 + DEFAULT_TOLERANCE, NEGATIVE_INFINITY)).isEqualTo(TOLERABLE_2);
+ assertThat(nextAfter(2.2 + DEFAULT_TOLERANCE, NEGATIVE_INFINITY)).isEqualTo(TOLERABLE_2POINT2);
+ assertThat(nextAfter(2.2 + DEFAULT_TOLERANCE, POSITIVE_INFINITY))
+ .isEqualTo(INTOLERABLE_2POINT2);
+ assertThat(nextAfter(2.2, POSITIVE_INFINITY)).isEqualTo(OVER_2POINT2);
+ assertThat(nextAfter(3.3 + DEFAULT_TOLERANCE, NEGATIVE_INFINITY)).isEqualTo(TOLERABLE_3POINT3);
+ assertThat(nextAfter(3.3 + DEFAULT_TOLERANCE, POSITIVE_INFINITY))
+ .isEqualTo(INTOLERABLE_3POINT3);
+ assertThat(nextAfter((double) Long.MIN_VALUE, NEGATIVE_INFINITY)).isEqualTo(UNDER_MIN_OF_LONG);
+ }
+
+ @Test
+ public void isEqualTo_WithoutToleranceParameter_Success() {
+ assertThat(array(2.2d, 5.4d, POSITIVE_INFINITY, NEGATIVE_INFINITY, 0.0, -0.0))
+ .isEqualTo(array(2.2d, 5.4d, POSITIVE_INFINITY, NEGATIVE_INFINITY, 0.0, -0.0));
+ }
+
+ @Test
+ @GwtIncompatible("gwt Arrays.equals(double[], double[])")
+ public void isEqualTo_WithoutToleranceParameter_NaN_Success() {
+ assertThat(array(2.2d, 5.4d, POSITIVE_INFINITY, NEGATIVE_INFINITY, NaN, 0.0, -0.0))
+ .isEqualTo(array(2.2d, 5.4d, POSITIVE_INFINITY, NEGATIVE_INFINITY, NaN, 0.0, -0.0));
+ }
+
+ @Test
+ public void isEqualTo_WithoutToleranceParameter_Fail_NotEqual() {
+ expectFailureWhenTestingThat(array(2.2d)).isEqualTo(array(OVER_2POINT2));
+ assertFailureValue("expected", "[2.2000000000000006]");
+ assertFailureValue("but was", "[2.2]");
+ assertFailureValue("differs at index", "[0]");
+ }
+
+ @Test
+ public void isEqualTo_WithoutToleranceParameter_Fail_DifferentOrder() {
+ expectFailureWhenTestingThat(array(2.2d, 3.3d)).isEqualTo(array(3.3d, 2.2d));
+ }
+
+ @Test
+ public void isEqualTo_WithoutToleranceParameter_Fail_Longer() {
+ expectFailureWhenTestingThat(array(2.2d, 3.3d)).isEqualTo(array(2.2d, 3.3d, 4.4d));
+ assertFailureKeys("expected", "but was", "wrong length", "expected", "but was");
+ assertFailureValueIndexed("expected", 1, "3");
+ assertFailureValueIndexed("but was", 1, "2");
+ }
+
+ @Test
+ public void isEqualTo_WithoutToleranceParameter_Fail_Shorter() {
+ expectFailureWhenTestingThat(array(2.2d, 3.3d)).isEqualTo(array(2.2d));
+ }
+
+ @Test
+ public void isEqualTo_WithoutToleranceParameter_Fail_PlusMinusZero() {
+ expectFailureWhenTestingThat(array(0.0d)).isEqualTo(array(-0.0d));
+ assertFailureValue("expected", "[-0.0]");
+ assertFailureValue("but was", "[0.0]");
+ }
+
+ @Test
+ public void isEqualTo_WithoutToleranceParameter_Fail_NotAnArray() {
+ expectFailureWhenTestingThat(array(2.2d, 3.3d, 4.4d)).isEqualTo(new Object());
+ }
+
+ @Test
+ public void isNotEqualTo_WithoutToleranceParameter_FailEquals() {
+ expectFailureWhenTestingThat(array(2.2d, 5.4d, POSITIVE_INFINITY, NEGATIVE_INFINITY))
+ .isNotEqualTo(array(2.2d, 5.4d, POSITIVE_INFINITY, NEGATIVE_INFINITY));
+ }
+
+ @Test
+ public void isNotEqualTo_WithoutToleranceParameter_NaN_plusZero_FailEquals() {
+ expectFailureWhenTestingThat(
+ array(2.2d, 5.4d, POSITIVE_INFINITY, NEGATIVE_INFINITY, NaN, 0.0, -0.0))
+ .isNotEqualTo(array(2.2d, 5.4d, POSITIVE_INFINITY, NEGATIVE_INFINITY, NaN, 0.0, -0.0));
+ }
+
+ @Test
+ public void isNotEqualTo_WithoutToleranceParameter_Success_NotEqual() {
+ assertThat(array(2.2d)).isNotEqualTo(array(OVER_2POINT2));
+ }
+
+ @Test
+ public void isNotEqualTo_WithoutToleranceParameter_Success_DifferentOrder() {
+ assertThat(array(2.2d, 3.3d)).isNotEqualTo(array(3.3d, 2.2d));
+ }
+
+ @Test
+ public void isNotEqualTo_WithoutToleranceParameter_Success_Longer() {
+ assertThat(array(2.2d, 3.3d)).isNotEqualTo(array(2.2d, 3.3d, 4.4d));
+ }
+
+ @Test
+ public void isNotEqualTo_WithoutToleranceParameter_Success_Shorter() {
+ assertThat(array(2.2d, 3.3d)).isNotEqualTo(array(2.2d));
+ }
+
+ @Test
+ public void isNotEqualTo_WithoutToleranceParameter_Success_PlusMinusZero() {
+ assertThat(array(0.0d)).isNotEqualTo(array(-0.0d));
+ }
+
+ @Test
+ public void isNotEqualTo_WithoutToleranceParameter_Success_NotAnArray() {
+ assertThat(array(2.2d, 3.3d, 4.4d)).isNotEqualTo(new Object());
+ }
+
+ @Test
+ public void usingTolerance_contains_success() {
+ assertThat(array(1.1, TOLERABLE_2POINT2, 3.3)).usingTolerance(DEFAULT_TOLERANCE).contains(2.2);
+ }
+
+ @Test
+ public void usingTolerance_contains_successWithExpectedLong() {
+ assertThat(array(1.0, TOLERABLE_2, 3.0)).usingTolerance(DEFAULT_TOLERANCE).contains(2L);
+ }
+
+ @Test
+ public void usingTolerance_contains_failure() {
+ expectFailureWhenTestingThat(array(1.1, INTOLERABLE_2POINT2, 3.3))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .contains(2.2);
+ assertFailureKeys("value of", "expected to contain", "testing whether", "but was");
+ assertFailureValue("value of", "array.asList()");
+ assertFailureValue("expected to contain", "2.2");
+ assertFailureValue(
+ "testing whether",
+ "actual element is a finite number within " + DEFAULT_TOLERANCE + " of expected element");
+ assertFailureValue("but was", "[1.1, " + INTOLERABLE_2POINT2 + ", 3.3]");
+ }
+
+ @Test
+ public void usingTolerance_contains_failureWithInfinity() {
+ expectFailureWhenTestingThat(array(1.1, POSITIVE_INFINITY, 3.3))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .contains(POSITIVE_INFINITY);
+ assertFailureKeys("value of", "expected to contain", "testing whether", "but was");
+ assertFailureValue("expected to contain", "Infinity");
+ assertFailureValue("but was", "[1.1, Infinity, 3.3]");
+ }
+
+ @Test
+ public void usingTolerance_contains_failureWithNaN() {
+ expectFailureWhenTestingThat(array(1.1, NaN, 3.3))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .contains(NaN);
+ assertFailureKeys("value of", "expected to contain", "testing whether", "but was");
+ assertFailureValue("expected to contain", "NaN");
+ assertFailureValue("but was", "[1.1, NaN, 3.3]");
+ }
+
+ @Test
+ public void usingTolerance_contains_successWithNegativeZero() {
+ assertThat(array(1.1, -0.0, 3.3)).usingTolerance(0.0).contains(0.0);
+ }
+
+ @Test
+ public void usingTolerance_contains_otherTypes() {
+ // Expected value is Float
+ assertThat(array(1.0, 2.0 + 0.5 * DEFAULT_TOLERANCE, 3.0))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .contains(2.0f);
+ // Expected value is Integer
+ assertThat(array(1.0, 2.0 + 0.5 * DEFAULT_TOLERANCE, 3.0))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .contains(2);
+ // Expected value is Integer.MAX_VALUE
+ assertThat(array(1.0, Integer.MAX_VALUE + 0.5 * DEFAULT_TOLERANCE, 3.0))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .contains(Integer.MAX_VALUE);
+ // Expected value is Long
+ assertThat(array(1.0, 2.0 + 0.5 * DEFAULT_TOLERANCE, 3.0))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .contains(2L);
+ // Expected value is Long.MIN_VALUE. This is -1*2^63, which has an exact double representation.
+ // For the actual value we use the next value down, which is is 2^11 smaller (because the
+ // resolution of doubles with absolute values between 2^63 and 2^64 is 2^11). So we'll make the
+ // assertion with a tolerance of 2^12.
+ assertThat(array(1.0, UNDER_MIN_OF_LONG, 3.0)).usingTolerance(1 << 12).contains(Long.MIN_VALUE);
+ // Expected value is BigInteger
+ assertThat(array(1.0, 2.0 + 0.5 * DEFAULT_TOLERANCE, 3.0))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .contains(BigInteger.valueOf(2));
+ // Expected value is BigDecimal
+ assertThat(array(1.0, 2.0 + 0.5 * DEFAULT_TOLERANCE, 3.0))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .contains(BigDecimal.valueOf(2.0));
+ }
+
+ @Test
+ public void usingTolerance_contains_nullExpected() {
+ expectFailureWhenTestingThat(array(1.1, 2.2, 3.3))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .contains(null);
+ assertFailureKeys(
+ "value of",
+ "expected to contain",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(1.1, null) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void usingTolerance_contains_negativeTolerance() {
+ try {
+ assertThat(array(1.1, 2.2, 3.3)).usingTolerance(-1.1 * DEFAULT_TOLERANCE).contains(2.0f);
+ fail("Expected IllegalArgumentException to be thrown but wasn't");
+ } catch (IllegalArgumentException expected) {
+ assertThat(expected)
+ .hasMessageThat()
+ .isEqualTo("tolerance (" + -1.1 * DEFAULT_TOLERANCE + ") cannot be negative");
+ }
+ }
+
+ @Test
+ public void usingTolerance_containsAtLeast_primitiveDoubleArray_success() {
+ assertThat(array(1.1, TOLERABLE_2POINT2, 3.3))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsAtLeast(array(2.2, 1.1));
+ }
+
+ @Test
+ public void usingTolerance_containsAtLeast_primitiveDoubleArray_failure() {
+ expectFailureWhenTestingThat(array(1.1, TOLERABLE_2POINT2, 3.3))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsAtLeast(array(2.2, 99.99));
+ assertFailureKeys(
+ "value of",
+ "missing (1)",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was");
+ assertFailureValue("missing (1)", "99.99");
+ }
+
+ @Test
+ public void usingTolerance_containsAtLeast_primitiveDoubleArray_inOrder_success() {
+ assertThat(array(1.1, TOLERABLE_2POINT2, 3.3))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsAtLeast(array(1.1, 2.2))
+ .inOrder();
+ }
+
+ @Test
+ public void usingTolerance_containsAtLeast_primitiveDoubleArray_inOrder_failure() {
+ expectFailureWhenTestingThat(array(1.1, TOLERABLE_2POINT2, 3.3))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsAtLeast(array(2.2, 1.1))
+ .inOrder();
+ assertFailureKeys(
+ "value of",
+ "required elements were all found, but order was wrong",
+ "expected order for required elements",
+ "testing whether",
+ "but was");
+ assertFailureValue("expected order for required elements", "[2.2, 1.1]");
+ }
+
+ @Test
+ public void usingTolerance_containsAnyOf_primitiveDoubleArray_success() {
+ assertThat(array(1.1, TOLERABLE_2POINT2, 3.3))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsAnyOf(array(99.99, 2.2));
+ }
+
+ @Test
+ public void usingTolerance_containsAnyOf_primitiveDoubleArray_failure() {
+ expectFailureWhenTestingThat(array(1.1, TOLERABLE_2POINT2, 3.3))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsAnyOf(array(99.99, 999.999));
+ assertFailureKeys("value of", "expected to contain any of", "testing whether", "but was");
+ assertFailureValue("expected to contain any of", "[99.99, 999.999]");
+ }
+
+ @Test
+ public void usingTolerance_containsExactly_primitiveDoubleArray_success() {
+ assertThat(array(1.1, TOLERABLE_2POINT2, 3.3))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsExactly(array(2.2, 1.1, 3.3));
+ }
+
+ @Test
+ public void usingTolerance_containsExactly_primitiveDoubleArray_failure() {
+ expectFailureWhenTestingThat(array(1.1, TOLERABLE_2POINT2, 3.3))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsExactly(array(2.2, 1.1));
+ assertFailureKeys(
+ "value of", "unexpected (1)", "---", "expected", "testing whether", "but was");
+ assertFailureValue("unexpected (1)", "3.3");
+ }
+
+ @Test
+ public void usingTolerance_containsExactly_primitiveDoubleArray_inOrder_success() {
+ assertThat(array(1.1, TOLERABLE_2POINT2, 3.3))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsExactly(array(1.1, 2.2, 3.3))
+ .inOrder();
+ }
+
+ @Test
+ public void usingTolerance_containsExactly_primitiveDoubleArray_inOrder_failure() {
+ expectFailureWhenTestingThat(array(1.1, TOLERABLE_2POINT2, 3.3))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsExactly(array(2.2, 1.1, 3.3))
+ .inOrder();
+ assertFailureKeys(
+ "value of",
+ "contents match, but order was wrong",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValue("expected", "[2.2, 1.1, 3.3]");
+ }
+
+ @Test
+ public void usingTolerance_containsNoneOf_primitiveDoubleArray_success() {
+ assertThat(array(1.1, TOLERABLE_2POINT2, 3.3))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsNoneOf(array(99.99, 999.999));
+ }
+
+ @Test
+ public void usingTolerance_containsNoneOf_primitiveDoubleArray_failure() {
+ expectFailureWhenTestingThat(array(1.1, TOLERABLE_2POINT2, 3.3))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsNoneOf(array(99.99, 2.2));
+ assertFailureKeys(
+ "value of",
+ "expected not to contain any of",
+ "testing whether",
+ "but contained",
+ "corresponding to",
+ "---",
+ "full contents");
+ assertFailureValue("expected not to contain any of", "[99.99, 2.2]");
+ assertFailureValue("but contained", "[" + TOLERABLE_2POINT2 + "]");
+ assertFailureValue("corresponding to", "2.2");
+ }
+
+ @Test
+ public void usingExactEquality_contains_success() {
+ assertThat(array(1.1, 2.2, 3.3)).usingExactEquality().contains(2.2);
+ }
+
+ @Test
+ public void usingExactEquality_contains_failure() {
+ expectFailureWhenTestingThat(array(1.1, OVER_2POINT2, 3.3)).usingExactEquality().contains(2.2);
+ assertFailureKeys("value of", "expected to contain", "testing whether", "but was");
+ assertFailureValue("expected to contain", "2.2");
+ assertFailureValue("testing whether", "actual element is exactly equal to expected element");
+ assertFailureValue("but was", "[1.1, " + OVER_2POINT2 + ", 3.3]");
+ }
+
+ @Test
+ public void usingExactEquality_contains_otherTypes() {
+ // Expected value is Float
+ assertThat(array(1.0, 2.0, 3.0)).usingExactEquality().contains(2.0f);
+ // Expected value is Integer
+ assertThat(array(1.0, 2.0, 3.0)).usingExactEquality().contains(2);
+ assertThat(array(1.0, Integer.MAX_VALUE, 3.0)).usingExactEquality().contains(Integer.MAX_VALUE);
+ // Expected value is Long - supported up to +/- 2^53
+ assertThat(array(1.0, 2.0, 3.0)).usingExactEquality().contains(2L);
+ assertThat(array(1.0, 1L << 53, 3.0)).usingExactEquality().contains(1L << 53);
+ }
+
+ @Test
+ public void usingExactEquality_contains_otherTypes_longOutOfRange() {
+ long expected = (1L << 53) + 1L;
+ expectFailureWhenTestingThat(array(1.1, 2.2, 3.3)).usingExactEquality().contains(expected);
+ assertFailureKeys(
+ "value of",
+ "expected to contain",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertFailureValue("expected to contain", Long.toString(expected));
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(1.1, " + expected + ") threw java.lang.IllegalArgumentException");
+ assertThatFailure()
+ .factValue("first exception")
+ .contains(
+ "Expected value "
+ + expected
+ + " in assertion using exact double equality was a long with an absolute value "
+ + "greater than 2^52 which has no exact double representation");
+ }
+
+ @Test
+ public void usingExactEquality_contains_otherTypes_bigIntegerNotSupported() {
+ BigInteger expected = BigInteger.valueOf(2);
+ expectFailureWhenTestingThat(array(1.1, 2.2, 3.3)).usingExactEquality().contains(expected);
+ assertFailureKeys(
+ "value of",
+ "expected to contain",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertFailureValue("expected to contain", "2");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(1.1, " + expected + ") threw java.lang.IllegalArgumentException");
+ assertThatFailure()
+ .factValue("first exception")
+ .contains(
+ "Expected value in assertion using exact double equality was of unsupported type "
+ + BigInteger.class
+ + " (it may not have an exact double representation)");
+ }
+
+ @Test
+ public void usingExactEquality_contains_otherTypes_bigDecimalNotSupported() {
+ BigDecimal expected = BigDecimal.valueOf(2.0);
+ expectFailureWhenTestingThat(array(1.1, 2.2, 3.3)).usingExactEquality().contains(expected);
+ assertFailureKeys(
+ "value of",
+ "expected to contain",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertFailureValue("expected to contain", expected.toString());
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(1.1, " + expected + ") threw java.lang.IllegalArgumentException");
+ assertThatFailure()
+ .factValue("first exception")
+ .contains(
+ "Expected value in assertion using exact double equality was of unsupported type "
+ + BigDecimal.class
+ + " (it may not have an exact double representation)");
+ }
+
+ @Test
+ public void usingExactEquality_contains_successWithInfinity() {
+ assertThat(array(1.1, POSITIVE_INFINITY, 3.3)).usingExactEquality().contains(POSITIVE_INFINITY);
+ }
+
+ @Test
+ public void usingExactEquality_contains_successWithNaN() {
+ assertThat(array(1.1, NaN, 3.3)).usingExactEquality().contains(NaN);
+ }
+
+ @Test
+ public void usingExactEquality_contains_failureWithNegativeZero() {
+ expectFailureWhenTestingThat(array(1.1, -0.0, 3.3)).usingExactEquality().contains(0.0);
+ assertFailureKeys("value of", "expected to contain", "testing whether", "but was");
+ /*
+ * TODO(cpovirk): Find a way to print "0.0" rather than 0 in the error, even under GWT. One
+ * easy(?) hack would be to make UsingCorrespondence use Platform.doubleToString() when
+ * applicable. Or maybe Correspondence implementations should be able to provide custom string
+ * conversions, similar to how we plan to let them render their own diffs.
+ */
+ assertFailureValue("expected to contain", Double.toString(0.0));
+ }
+
+ @Test
+ public void usingExactEquality_contains_nullExpected() {
+ expectFailureWhenTestingThat(array(1.1, 2.2, 3.3)).usingExactEquality().contains(null);
+ assertFailureKeys(
+ "value of",
+ "expected to contain",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertFailureValue("expected to contain", "null");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(1.1, null) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void usingExactEquality_containsAtLeast_primitiveDoubleArray_success() {
+ assertThat(array(1.1, 2.2, 3.3)).usingExactEquality().containsAtLeast(array(2.2, 1.1));
+ }
+
+ @Test
+ public void usingExactEquality_containsAtLeast_primitiveDoubleArray_failure() {
+ expectFailureWhenTestingThat(array(1.1, 2.2, 3.3))
+ .usingExactEquality()
+ .containsAtLeast(array(2.2, 99.99));
+ assertFailureKeys(
+ "value of",
+ "missing (1)",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was");
+ assertFailureValue("missing (1)", "99.99");
+ }
+
+ @Test
+ public void usingExactEquality_containsAtLeast_primitiveDoubleArray_inOrder_success() {
+ assertThat(array(1.1, 2.2, 3.3))
+ .usingExactEquality()
+ .containsAtLeast(array(1.1, 2.2))
+ .inOrder();
+ }
+
+ @Test
+ public void usingExactEquality_containsAtLeast_primitiveDoubleArray_inOrder_failure() {
+ expectFailureWhenTestingThat(array(1.1, 2.2, 3.3))
+ .usingExactEquality()
+ .containsAtLeast(array(2.2, 1.1))
+ .inOrder();
+ assertFailureKeys(
+ "value of",
+ "required elements were all found, but order was wrong",
+ "expected order for required elements",
+ "testing whether",
+ "but was");
+ assertFailureValue("expected order for required elements", "[2.2, 1.1]");
+ }
+
+ @Test
+ public void usingExactEquality_containsAnyOf_primitiveDoubleArray_success() {
+ assertThat(array(1.1, 2.2, 3.3)).usingExactEquality().containsAnyOf(array(99.99, 2.2));
+ }
+
+ @Test
+ public void usingExactEquality_containsAnyOf_primitiveDoubleArray_failure() {
+ expectFailureWhenTestingThat(array(1.1, 2.2, 3.3))
+ .usingExactEquality()
+ .containsAnyOf(array(99.99, 999.999));
+ assertFailureKeys("value of", "expected to contain any of", "testing whether", "but was");
+ }
+
+ @Test
+ public void usingExactEquality_containsExactly_primitiveDoubleArray_success() {
+ assertThat(array(1.1, 2.2, 3.3)).usingExactEquality().containsExactly(array(2.2, 1.1, 3.3));
+ }
+
+ @Test
+ public void usingExactEquality_containsExactly_primitiveDoubleArray_failure() {
+ expectFailureWhenTestingThat(array(1.1, 2.2, 3.3))
+ .usingExactEquality()
+ .containsExactly(array(2.2, 1.1));
+ assertFailureKeys(
+ "value of", "unexpected (1)", "---", "expected", "testing whether", "but was");
+ assertFailureValue("unexpected (1)", "3.3");
+ }
+
+ @Test
+ public void usingExactEquality_containsExactly_primitiveDoubleArray_inOrder_success() {
+ assertThat(array(1.1, 2.2, 3.3))
+ .usingExactEquality()
+ .containsExactly(array(1.1, 2.2, 3.3))
+ .inOrder();
+ }
+
+ @Test
+ public void usingExactEquality_containsExactly_primitiveDoubleArray_inOrder_failure() {
+ expectFailureWhenTestingThat(array(1.1, 2.2, 3.3))
+ .usingExactEquality()
+ .containsExactly(array(2.2, 1.1, 3.3))
+ .inOrder();
+ assertFailureKeys(
+ "value of",
+ "contents match, but order was wrong",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValue("expected", "[2.2, 1.1, 3.3]");
+ }
+
+ @Test
+ public void usingExactEquality_containsNoneOf_primitiveDoubleArray_success() {
+ assertThat(array(1.1, 2.2, 3.3)).usingExactEquality().containsNoneOf(array(99.99, 999.999));
+ }
+
+ @Test
+ public void usingExactEquality_containsNoneOf_primitiveDoubleArray_failure() {
+ expectFailureWhenTestingThat(array(1.1, 2.2, 3.3))
+ .usingExactEquality()
+ .containsNoneOf(array(99.99, 2.2));
+ assertFailureKeys(
+ "value of",
+ "expected not to contain any of",
+ "testing whether",
+ "but contained",
+ "corresponding to",
+ "---",
+ "full contents");
+ assertFailureValue("expected not to contain any of", "[99.99, 2.2]");
+ assertFailureValue("but contained", "[2.2]");
+ assertFailureValue("corresponding to", "2.2");
+ }
+
+ @Test
+ public void smallDifferenceInLongRepresentation() {
+ expectFailureWhenTestingThat(array(-4.4501477170144023E-308))
+ .isEqualTo(array(-4.450147717014402E-308));
+ }
+
+ @Test
+ public void noCommas() {
+ // Maybe we should include commas, but we don't yet, so make sure we don't under GWT, either.
+ expectFailureWhenTestingThat(array(10000.0)).isEqualTo(array(20000.0));
+ assertFailureValue("expected", "[20000.0]");
+ assertFailureValue("but was", "[10000.0]");
+ }
+
+ private static double[] array(double... primitives) {
+ return primitives;
+ }
+
+ private PrimitiveDoubleArraySubject expectFailureWhenTestingThat(double[] actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/PrimitiveFloatArraySubjectTest.java b/core/src/test/java/com/google/common/truth/PrimitiveFloatArraySubjectTest.java
new file mode 100644
index 00000000..7abb437b
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/PrimitiveFloatArraySubjectTest.java
@@ -0,0 +1,726 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Strings.lenientFormat;
+import static com.google.common.truth.Platform.floatToString;
+import static com.google.common.truth.Truth.assertThat;
+import static java.lang.Float.NEGATIVE_INFINITY;
+import static java.lang.Float.NaN;
+import static java.lang.Float.POSITIVE_INFINITY;
+import static java.lang.Math.nextAfter;
+import static org.junit.Assert.fail;
+
+import com.google.common.annotations.GwtIncompatible;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link PrimitiveFloatArraySubject}.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+@RunWith(JUnit4.class)
+public class PrimitiveFloatArraySubjectTest extends BaseSubjectTestCase {
+ private static final float DEFAULT_TOLERANCE = 0.000005f;
+
+ private static final float JUST_OVER_2POINT2 = 2.2000003f;
+ private static final float JUST_OVER_3POINT3 = 3.3000002f;
+ private static final float TOLERABLE_3POINT3 = 3.3000047f;
+ private static final float INTOLERABLE_3POINT3 = 3.3000052f;
+ private static final float UNDER_LONG_MIN = -9.223373E18f;
+ private static final float TOLERABLE_TWO = 2.0000048f;
+ private static final float TOLERABLE_2POINT2 = 2.2000048f;
+ private static final float INTOLERABLE_2POINT2 = 2.2000053f;
+
+ @Test
+ @GwtIncompatible("Math.nextAfter")
+ public void testFloatConstants_matchNextAfter() {
+ assertThat(nextAfter(2.2f, POSITIVE_INFINITY)).isEqualTo(JUST_OVER_2POINT2);
+ assertThat(nextAfter(3.3f, POSITIVE_INFINITY)).isEqualTo(JUST_OVER_3POINT3);
+ assertThat(nextAfter(3.3f + DEFAULT_TOLERANCE, NEGATIVE_INFINITY)).isEqualTo(TOLERABLE_3POINT3);
+ assertThat(nextAfter(3.3f + DEFAULT_TOLERANCE, POSITIVE_INFINITY))
+ .isEqualTo(INTOLERABLE_3POINT3);
+ assertThat(nextAfter(Long.MIN_VALUE, NEGATIVE_INFINITY)).isEqualTo(UNDER_LONG_MIN);
+ assertThat(nextAfter(2.2f + DEFAULT_TOLERANCE, NEGATIVE_INFINITY)).isEqualTo(TOLERABLE_2POINT2);
+ assertThat(nextAfter(2.2f + DEFAULT_TOLERANCE, POSITIVE_INFINITY))
+ .isEqualTo(INTOLERABLE_2POINT2);
+ }
+
+ @Test
+ public void isEqualTo_WithoutToleranceParameter_Success() {
+ assertThat(array(2.2f, 5.4f, POSITIVE_INFINITY, NEGATIVE_INFINITY, NaN, 0.0f, -0.0f))
+ .isEqualTo(array(2.2f, 5.4f, POSITIVE_INFINITY, NEGATIVE_INFINITY, NaN, 0.0f, -0.0f));
+ }
+
+ @Test
+ public void isEqualTo_WithoutToleranceParameter_Fail_NotEqual() {
+ expectFailureWhenTestingThat(array(2.2f)).isEqualTo(array(JUST_OVER_2POINT2));
+ assertFailureKeys("expected", "but was", "differs at index");
+ assertFailureValue("expected", "[" + floatToString(JUST_OVER_2POINT2) + "]");
+ assertFailureValue("but was", "[" + floatToString(2.2f) + "]");
+ assertFailureValue("differs at index", "[0]");
+ }
+
+ @Test
+ public void isEqualTo_WithoutToleranceParameter_Fail_DifferentOrder() {
+ expectFailureWhenTestingThat(array(2.2f, 3.3f)).isEqualTo(array(3.3f, 2.2f));
+ }
+
+ @Test
+ public void isEqualTo_WithoutToleranceParameter_Fail_Longer() {
+ expectFailureWhenTestingThat(array(2.2f, 3.3f)).isEqualTo(array(2.2f, 3.3f, 4.4f));
+ assertFailureKeys("expected", "but was", "wrong length", "expected", "but was");
+ assertFailureValueIndexed("expected", 1, "3");
+ assertFailureValueIndexed("but was", 1, "2");
+ }
+
+ @Test
+ public void isEqualTo_WithoutToleranceParameter_Fail_Shorter() {
+ expectFailureWhenTestingThat(array(2.2f, 3.3f)).isEqualTo(array(2.2f));
+ }
+
+ @Test
+ public void isEqualTo_WithoutToleranceParameter_Fail_PlusMinusZero() {
+ expectFailureWhenTestingThat(array(0.0f)).isEqualTo(array(-0.0f));
+ assertFailureValue("expected", "[-0.0]");
+ assertFailureValue("but was", "[0.0]");
+ }
+
+ @Test
+ public void isEqualTo_WithoutToleranceParameter_Fail_NotAnArray() {
+ expectFailureWhenTestingThat(array(2.2f, 3.3f, 4.4f)).isEqualTo(new Object());
+ }
+
+ @Test
+ public void isNotEqualTo_WithoutToleranceParameter_FailEquals() {
+ expectFailureWhenTestingThat(
+ array(2.2f, 5.4f, POSITIVE_INFINITY, NEGATIVE_INFINITY, NaN, 0.0f, -0.0f))
+ .isNotEqualTo(array(2.2f, 5.4f, POSITIVE_INFINITY, NEGATIVE_INFINITY, NaN, 0.0f, -0.0f));
+ }
+
+ @Test
+ public void isNotEqualTo_WithoutToleranceParameter_Success_NotEqual() {
+ assertThat(array(2.2f)).isNotEqualTo(array(JUST_OVER_2POINT2));
+ }
+
+ @Test
+ public void isNotEqualTo_WithoutToleranceParameter_Success_DifferentOrder() {
+ assertThat(array(2.2f, 3.3f)).isNotEqualTo(array(3.3f, 2.2f));
+ }
+
+ @Test
+ public void isNotEqualTo_WithoutToleranceParameter_Success_Longer() {
+ assertThat(array(2.2f, 3.3f)).isNotEqualTo(array(2.2f, 3.3f, 4.4f));
+ }
+
+ @Test
+ public void isNotEqualTo_WithoutToleranceParameter_Success_Shorter() {
+ assertThat(array(2.2f, 3.3f)).isNotEqualTo(array(2.2f));
+ }
+
+ @Test
+ public void isNotEqualTo_WithoutToleranceParameter_Success_PlusMinusZero() {
+ assertThat(array(0.0f)).isNotEqualTo(array(-0.0f));
+ }
+
+ @Test
+ public void isNotEqualTo_WithoutToleranceParameter_Success_NotAnArray() {
+ assertThat(array(2.2f, 3.3f, 4.4f)).isNotEqualTo(new Object());
+ }
+
+ @Test
+ public void usingTolerance_contains_success() {
+ assertThat(array(1.1f, TOLERABLE_2POINT2, 3.2f))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .contains(2.2f);
+ }
+
+ @Test
+ public void usingTolerance_contains_successWithExpectedLong() {
+ assertThat(array(1.0f, TOLERABLE_TWO, 3.0f)).usingTolerance(DEFAULT_TOLERANCE).contains(2L);
+ }
+
+ @Test
+ public void usingTolerance_contains_failure() {
+ expectFailureWhenTestingThat(array(1.1f, INTOLERABLE_2POINT2, 3.3f))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .contains(2.0f);
+ assertFailureKeys("value of", "expected to contain", "testing whether", "but was");
+ assertFailureValue("expected to contain", Float.toString(2.0f));
+ assertFailureValue(
+ "testing whether",
+ "actual element is a finite number within "
+ + (double) DEFAULT_TOLERANCE
+ + " of expected element");
+ assertFailureValue("but was", "[" + 1.1f + ", " + INTOLERABLE_2POINT2 + ", " + 3.3f + "]");
+ }
+
+ @Test
+ public void usingTolerance_contains_failureWithInfinity() {
+ expectFailureWhenTestingThat(array(1.1f, POSITIVE_INFINITY, 3.3f))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .contains(POSITIVE_INFINITY);
+ assertFailureKeys("value of", "expected to contain", "testing whether", "but was");
+ assertFailureValue("expected to contain", "Infinity");
+ assertFailureValue("but was", "[" + 1.1f + ", Infinity, " + 3.3f + "]");
+ }
+
+ @Test
+ public void usingTolerance_contains_failureWithNaN() {
+ expectFailureWhenTestingThat(array(1.1f, NaN, 3.3f))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .contains(NaN);
+ assertFailureKeys("value of", "expected to contain", "testing whether", "but was");
+ assertFailureValue("expected to contain", "NaN");
+ assertFailureValue("but was", "[" + 1.1f + ", NaN, " + 3.3f + "]");
+ }
+
+ @Test
+ public void usingTolerance_contains_successWithNegativeZero() {
+ assertThat(array(1.0f, -0.0f, 3.0f)).usingTolerance(0.0f).contains(0.0f);
+ }
+
+ @Test
+ public void usingTolerance_contains_otherTypes() {
+ // Expected value is Double
+ assertThat(array(1.0f, 2.0f + 0.5f * DEFAULT_TOLERANCE, 3.0f))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .contains(2.0);
+ // Expected value is Integer
+ assertThat(array(1.0f, 2.0f + 0.5f * DEFAULT_TOLERANCE, 3.0f))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .contains(2);
+ // Expected value is Integer.MIN_VALUE. This is -1*2^31, which has an exact float
+ // representation. For the actual value we use the next value down, which is 2^8 smaller
+ // (because the resolution of floats with absolute values between 2^31 and 2^32 is 2^8). So
+ // we'll make the assertion with a tolerance of 2^9.
+ assertThat(array(1.0f, Integer.MIN_VALUE + 0.5f * DEFAULT_TOLERANCE, 3.0f))
+ .usingTolerance(1 << 9)
+ .contains(Integer.MIN_VALUE);
+ // Expected value is Long
+ assertThat(array(1.0f, 2.0f + 0.5f * DEFAULT_TOLERANCE, 3.0f))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .contains(2L);
+ // Expected value is Long.MIN_VALUE. This is -1*2^63, which has an exact float representation.
+ // For the actual value we use the next value down, which is is 2^40 smaller (because the
+ // resolution of floats with absolute values between 2^63 and 2^64 is 2^40). So we'll make the
+ // assertion with a tolerance of 2^41.
+ assertThat(array(1.0f, UNDER_LONG_MIN, 3.0f)).usingTolerance(1L << 41).contains(Long.MIN_VALUE);
+ // Expected value is BigInteger
+ assertThat(array(1.0f, 2.0f + 0.5f * DEFAULT_TOLERANCE, 3.0f))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .contains(BigInteger.valueOf(2));
+ // Expected value is BigDecimal
+ assertThat(array(1.0f, 2.0f + 0.5f * DEFAULT_TOLERANCE, 3.0f))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .contains(BigDecimal.valueOf(2.0));
+ }
+
+ @Test
+ public void usingTolerance_contains_nullExpected() {
+ float[] actual = array(1.0f, 2.0f, 3.0f);
+ expectFailureWhenTestingThat(actual).usingTolerance(DEFAULT_TOLERANCE).contains(null);
+ assertFailureKeys(
+ "value of",
+ "expected to contain",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(" + actual[0] + ", null) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void usingTolerance_contains_negativeTolerance() {
+ try {
+ assertThat(array(1.0f, 2.0f, 3.0f)).usingTolerance(-1.0f * DEFAULT_TOLERANCE).contains(2.0f);
+ fail("Expected IllegalArgumentException to be thrown but wasn't");
+ } catch (IllegalArgumentException expected) {
+ assertThat(expected)
+ .hasMessageThat()
+ .isEqualTo("tolerance (" + -1.0 * DEFAULT_TOLERANCE + ") cannot be negative");
+ }
+ }
+
+ @Test
+ public void usingTolerance_containsAtLeast_primitiveFloatArray_success() {
+ assertThat(array(1.1f, TOLERABLE_2POINT2, 3.3f))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsAtLeast(array(2.2f, 1.1f));
+ }
+
+ @Test
+ public void usingTolerance_containsAtLeast_primitiveFloatArray_failure() {
+ expectFailureWhenTestingThat(array(1.1f, TOLERABLE_2POINT2, 3.3f))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsAtLeast(array(2.2f, 99.99f));
+ assertFailureKeys(
+ "value of",
+ "missing (1)",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was");
+ assertFailureValue("missing (1)", Float.toString(99.99f));
+ }
+
+ @Test
+ public void usingTolerance_containsAtLeast_primitiveFloatArray_inOrder_success() {
+ assertThat(array(1.1f, TOLERABLE_2POINT2, 3.3f))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsAtLeast(array(1.1f, 2.2f))
+ .inOrder();
+ }
+
+ @Test
+ public void usingTolerance_containsAtLeast_primitiveFloatArray_inOrder_failure() {
+ expectFailureWhenTestingThat(array(1.1f, TOLERABLE_2POINT2, 3.3f))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsAtLeast(array(2.2f, 1.1f))
+ .inOrder();
+ assertFailureKeys(
+ "value of",
+ "required elements were all found, but order was wrong",
+ "expected order for required elements",
+ "testing whether",
+ "but was");
+ assertFailureValue(
+ "expected order for required elements", lenientFormat("[%s, %s]", 2.2f, 1.1f));
+ }
+
+ @Test
+ public void usingTolerance_containsAnyOf_primitiveFloatArray_success() {
+ assertThat(array(1.0f, TOLERABLE_2POINT2, 3.0f))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsAnyOf(array(99.99f, 2.2f));
+ }
+
+ @Test
+ public void usingTolerance_containsAnyOf_primitiveFloatArray_failure() {
+ expectFailureWhenTestingThat(array(1.1f, TOLERABLE_2POINT2, 3.3f))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsAnyOf(array(99.99f, 999.999f));
+ assertFailureKeys("value of", "expected to contain any of", "testing whether", "but was");
+ assertFailureValue("expected to contain any of", "[" + 99.99f + ", " + 999.999f + "]");
+ }
+
+ @Test
+ public void usingTolerance_containsExactly_primitiveFloatArray_success() {
+ assertThat(array(1.1f, TOLERABLE_2POINT2, 3.3f))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsExactly(array(2.2f, 1.1f, 3.3f));
+ }
+
+ @Test
+ public void usingTolerance_containsExactly_primitiveFloatArray_failure() {
+ expectFailureWhenTestingThat(array(1.1f, TOLERABLE_2POINT2, 3.3f))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsExactly(array(2.2f, 1.1f));
+ assertFailureKeys(
+ "value of", "unexpected (1)", "---", "expected", "testing whether", "but was");
+ assertFailureValue("unexpected (1)", Float.toString(3.3f));
+ }
+
+ @Test
+ public void usingTolerance_containsExactly_primitiveFloatArray_inOrder_success() {
+ assertThat(array(1.1f, TOLERABLE_2POINT2, 3.3f))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsExactly(array(1.1f, 2.2f, 3.3f))
+ .inOrder();
+ }
+
+ @Test
+ public void usingTolerance_containsExactly_primitiveFloatArray_inOrder_failure() {
+ expectFailureWhenTestingThat(array(1.1f, TOLERABLE_2POINT2, 3.3f))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsExactly(array(2.2f, 1.1f, 3.3f))
+ .inOrder();
+ assertFailureKeys(
+ "value of",
+ "contents match, but order was wrong",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValue("expected", lenientFormat("[%s, %s, %s]", 2.2f, 1.1f, 3.3f));
+ }
+
+ @Test
+ public void usingTolerance_containsNoneOf_primitiveFloatArray_success() {
+ assertThat(array(1.1f, TOLERABLE_2POINT2, 3.3f))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsNoneOf(array(99.99f, 999.999f));
+ }
+
+ @Test
+ public void usingTolerance_containsNoneOf_primitiveFloatArray_failure() {
+ expectFailureWhenTestingThat(array(1.1f, TOLERABLE_2POINT2, 3.3f))
+ .usingTolerance(DEFAULT_TOLERANCE)
+ .containsNoneOf(array(99.99f, 2.2f));
+ assertFailureKeys(
+ "value of",
+ "expected not to contain any of",
+ "testing whether",
+ "but contained",
+ "corresponding to",
+ "---",
+ "full contents");
+ assertFailureValue("expected not to contain any of", "[" + 99.99f + ", " + 2.2f + "]");
+ assertFailureValue("but contained", "[" + TOLERABLE_2POINT2 + "]");
+ assertFailureValue("corresponding to", Float.toString(2.2f));
+ }
+
+ @Test
+ public void usingExactEquality_contains_success() {
+ assertThat(array(1.0f, 2.0f, 3.0f)).usingExactEquality().contains(2.0f);
+ }
+
+ @Test
+ public void usingExactEquality_contains_failure() {
+ expectFailureWhenTestingThat(array(1.1f, JUST_OVER_2POINT2, 3.3f))
+ .usingExactEquality()
+ .contains(2.2f);
+ assertFailureKeys("value of", "expected to contain", "testing whether", "but was");
+ assertFailureValue("expected to contain", Float.toString(2.2f));
+ assertFailureValue("testing whether", "actual element is exactly equal to expected element");
+ assertFailureValue("but was", "[" + 1.1f + ", " + JUST_OVER_2POINT2 + ", " + 3.3f + "]");
+ }
+
+ @Test
+ public void usingExactEquality_contains_otherTypes() {
+ // Expected value is Integer - supported up to +/- 2^24
+ assertThat(array(1.0f, 2.0f, 3.0f)).usingExactEquality().contains(2);
+ assertThat(array(1.0f, 1 << 24, 3.0f)).usingExactEquality().contains(1 << 24);
+ // Expected value is Long - supported up to +/- 2^24
+ assertThat(array(1.0f, 2.0f, 3.0f)).usingExactEquality().contains(2L);
+ assertThat(array(1.0f, 1 << 24, 3.0f)).usingExactEquality().contains(1L << 24);
+ }
+
+ @Test
+ public void usingExactEquality_contains_otherTypes_intOutOfRange() {
+ int expected = (1 << 24) + 1;
+ float[] actual = array(1.0f, 2.0f, 3.0f);
+ expectFailureWhenTestingThat(actual).usingExactEquality().contains(expected);
+ assertFailureKeys(
+ "value of",
+ "expected to contain",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith(
+ "compare("
+ + actual[0]
+ + ", "
+ + expected
+ + ") threw java.lang.IllegalArgumentException");
+ assertThatFailure()
+ .factValue("first exception")
+ .contains(
+ "Expected value "
+ + expected
+ + " in assertion using exact float equality was an int with an absolute value "
+ + "greater than 2^24 which has no exact float representation");
+ }
+
+ @Test
+ public void usingExactEquality_contains_otherTypes_longOutOfRange() {
+ long expected = (1L << 24) + 1L;
+ float[] actual = array(1.0f, 2.0f, 3.0f);
+ expectFailureWhenTestingThat(actual).usingExactEquality().contains(expected);
+ assertFailureKeys(
+ "value of",
+ "expected to contain",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertFailureValue("expected to contain", Long.toString(expected));
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith(
+ "compare("
+ + actual[0]
+ + ", "
+ + expected
+ + ") threw java.lang.IllegalArgumentException");
+ assertThatFailure()
+ .factValue("first exception")
+ .contains(
+ "Expected value "
+ + expected
+ + " in assertion using exact float equality was a long with an absolute value "
+ + "greater than 2^24 which has no exact float representation");
+ }
+
+ @Test
+ public void usingExactEquality_contains_otherTypes_doubleNotSupported() {
+ double expected = 2.0;
+ float[] actual = array(1.0f, 2.0f, 3.0f);
+ expectFailureWhenTestingThat(actual).usingExactEquality().contains(expected);
+ assertFailureKeys(
+ "value of",
+ "expected to contain",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith(
+ "compare("
+ + actual[0]
+ + ", "
+ + expected
+ + ") threw java.lang.IllegalArgumentException");
+ assertThatFailure()
+ .factValue("first exception")
+ .contains(
+ "Expected value in assertion using exact float equality was a double, which is not "
+ + "supported as a double may not have an exact float representation");
+ }
+
+ @Test
+ public void usingExactEquality_contains_otherTypes_bigIntegerNotSupported() {
+ BigInteger expected = BigInteger.valueOf(2);
+ float[] actual = array(1.0f, 2.0f, 3.0f);
+ expectFailureWhenTestingThat(actual).usingExactEquality().contains(expected);
+ assertFailureKeys(
+ "value of",
+ "expected to contain",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertFailureValue("expected to contain", "2");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith(
+ "compare("
+ + actual[0]
+ + ", "
+ + expected
+ + ") threw java.lang.IllegalArgumentException");
+ assertThatFailure()
+ .factValue("first exception")
+ .contains(
+ "Expected value in assertion using exact float equality was of unsupported type "
+ + BigInteger.class
+ + " (it may not have an exact float representation)");
+ }
+
+ @Test
+ public void usingExactEquality_contains_otherTypes_bigDecimalNotSupported() {
+ BigDecimal expected = BigDecimal.valueOf(2.0);
+ float[] actual = array(1.0f, 2.0f, 3.0f);
+ expectFailureWhenTestingThat(actual).usingExactEquality().contains(expected);
+ assertFailureKeys(
+ "value of",
+ "expected to contain",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertFailureValue("expected to contain", expected.toString());
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith(
+ "compare("
+ + actual[0]
+ + ", "
+ + expected
+ + ") threw java.lang.IllegalArgumentException");
+ assertThatFailure()
+ .factValue("first exception")
+ .contains(
+ "Expected value in assertion using exact float equality was of unsupported type "
+ + BigDecimal.class
+ + " (it may not have an exact float representation)");
+ }
+
+ @Test
+ public void usingExactEquality_contains_successWithInfinity() {
+ assertThat(array(1.0f, POSITIVE_INFINITY, 3.0f))
+ .usingExactEquality()
+ .contains(POSITIVE_INFINITY);
+ }
+
+ @Test
+ public void usingExactEquality_contains_successWithNaN() {
+ assertThat(array(1.0f, NaN, 3.0f)).usingExactEquality().contains(NaN);
+ }
+
+ @Test
+ public void usingExactEquality_contains_failureWithNegativeZero() {
+ expectFailureWhenTestingThat(array(1.0f, -0.0f, 3.0f)).usingExactEquality().contains(0.0f);
+ assertFailureKeys("value of", "expected to contain", "testing whether", "but was");
+ assertFailureValue("expected to contain", Float.toString(0.0f));
+ }
+
+ @Test
+ public void usingExactEquality_contains_nullExpected() {
+ float[] actual = array(1.0f, 2.0f, 3.0f);
+ expectFailureWhenTestingThat(actual).usingExactEquality().contains(null);
+ assertFailureKeys(
+ "value of",
+ "expected to contain",
+ "testing whether",
+ "but was",
+ "additionally, one or more exceptions were thrown while comparing elements",
+ "first exception");
+ assertFailureValue("expected to contain", "null");
+ assertThatFailure()
+ .factValue("first exception")
+ .startsWith("compare(" + actual[0] + ", null) threw java.lang.NullPointerException");
+ }
+
+ @Test
+ public void usingExactEquality_containsAtLeast_primitiveFloatArray_success() {
+ assertThat(array(1.0f, 2.0f, 3.0f)).usingExactEquality().containsAtLeast(array(2.0f, 1.0f));
+ }
+
+ @Test
+ public void usingExactEquality_containsAtLeast_primitiveFloatArray_failure() {
+ expectFailureWhenTestingThat(array(1.1f, 2.2f, 3.3f))
+ .usingExactEquality()
+ .containsAtLeast(array(2.2f, 99.99f));
+ assertFailureKeys(
+ "value of",
+ "missing (1)",
+ "---",
+ "expected to contain at least",
+ "testing whether",
+ "but was");
+ assertFailureValue("missing (1)", Float.toString(99.99f));
+ }
+
+ @Test
+ public void usingExactEquality_containsAtLeast_primitiveFloatArray_inOrder_success() {
+ assertThat(array(1.0f, 2.0f, 3.0f))
+ .usingExactEquality()
+ .containsAtLeast(array(1.0f, 2.0f))
+ .inOrder();
+ }
+
+ @Test
+ public void usingExactEquality_containsAtLeast_primitiveFloatArray_inOrder_failure() {
+ expectFailureWhenTestingThat(array(1.1f, 2.2f, 3.3f))
+ .usingExactEquality()
+ .containsAtLeast(array(2.2f, 1.1f))
+ .inOrder();
+ assertFailureKeys(
+ "value of",
+ "required elements were all found, but order was wrong",
+ "expected order for required elements",
+ "testing whether",
+ "but was");
+ assertFailureValue(
+ "expected order for required elements", lenientFormat("[%s, %s]", 2.2f, 1.1f));
+ }
+
+ @Test
+ public void usingExactEquality_containsAnyOf_primitiveFloatArray_success() {
+ assertThat(array(1.0f, 2.0f, 3.0f)).usingExactEquality().containsAnyOf(array(99.99f, 2.0f));
+ }
+
+ @Test
+ public void usingExactEquality_containsAnyOf_primitiveFloatArray_failure() {
+ expectFailureWhenTestingThat(array(1.1f, 2.2f, 3.3f))
+ .usingExactEquality()
+ .containsAnyOf(array(99.99f, 999.999f));
+ assertFailureKeys("value of", "expected to contain any of", "testing whether", "but was");
+ }
+
+ @Test
+ public void usingExactEquality_containsExactly_primitiveFloatArray_success() {
+ assertThat(array(1.0f, 2.0f, 3.0f))
+ .usingExactEquality()
+ .containsExactly(array(2.0f, 1.0f, 3.0f));
+ }
+
+ @Test
+ public void usingExactEquality_containsExactly_primitiveFloatArray_failure() {
+ expectFailureWhenTestingThat(array(1.1f, 2.2f, 3.3f))
+ .usingExactEquality()
+ .containsExactly(array(2.2f, 1.1f));
+ assertFailureKeys(
+ "value of", "unexpected (1)", "---", "expected", "testing whether", "but was");
+ assertFailureValue("unexpected (1)", Float.toString(3.3f));
+ }
+
+ @Test
+ public void usingExactEquality_containsExactly_primitiveFloatArray_inOrder_success() {
+ assertThat(array(1.0f, 2.0f, 3.0f))
+ .usingExactEquality()
+ .containsExactly(array(1.0f, 2.0f, 3.0f))
+ .inOrder();
+ }
+
+ @Test
+ public void usingExactEquality_containsExactly_primitiveFloatArray_inOrder_failure() {
+ expectFailureWhenTestingThat(array(1.1f, 2.2f, 3.3f))
+ .usingExactEquality()
+ .containsExactly(array(2.2f, 1.1f, 3.3f))
+ .inOrder();
+ assertFailureKeys(
+ "value of",
+ "contents match, but order was wrong",
+ "expected",
+ "testing whether",
+ "but was");
+ assertFailureValue("expected", lenientFormat("[%s, %s, %s]", 2.2f, 1.1f, 3.3f));
+ }
+
+ @Test
+ public void usingExactEquality_containsNoneOf_primitiveFloatArray_success() {
+ assertThat(array(1.0f, 2.0f, 3.0f))
+ .usingExactEquality()
+ .containsNoneOf(array(99.99f, 999.999f));
+ }
+
+ @Test
+ public void usingExactEquality_containsNoneOf_primitiveFloatArray_failure() {
+ expectFailureWhenTestingThat(array(1.1f, 2.2f, 3.3f))
+ .usingExactEquality()
+ .containsNoneOf(array(99.99f, 2.2f));
+ assertFailureKeys(
+ "value of",
+ "expected not to contain any of",
+ "testing whether",
+ "but contained",
+ "corresponding to",
+ "---",
+ "full contents");
+ assertFailureValue("expected not to contain any of", "[" + 99.99f + ", " + 2.2f + "]");
+ assertFailureValue("but contained", "[" + 2.2f + "]");
+ assertFailureValue("corresponding to", Float.toString(2.2f));
+ }
+
+ private static float[] array(float... primitives) {
+ return primitives;
+ }
+
+ private PrimitiveFloatArraySubject expectFailureWhenTestingThat(float[] actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/PrimitiveIntArraySubjectTest.java b/core/src/test/java/com/google/common/truth/PrimitiveIntArraySubjectTest.java
new file mode 100644
index 00000000..a18a49a3
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/PrimitiveIntArraySubjectTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link com.google.common.truth.PrimitiveIntArraySubject}.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+@RunWith(JUnit4.class)
+public class PrimitiveIntArraySubjectTest extends BaseSubjectTestCase {
+ private static final int[] EMPTY = new int[0];
+
+ @Test
+ public void isEqualTo() {
+ assertThat(array(2, 5)).isEqualTo(array(2, 5));
+ }
+
+ @SuppressWarnings("TruthSelfEquals")
+ @Test
+ public void isEqualTo_Same() {
+ int[] same = array(2, 5);
+ assertThat(same).isEqualTo(same);
+ }
+
+ @Test
+ public void asList() {
+ assertThat(array(5, 2, 9)).asList().containsAtLeast(2, 9);
+ }
+
+ @Test
+ public void hasLength() {
+ assertThat(EMPTY).hasLength(0);
+ assertThat(array(2, 5)).hasLength(2);
+ }
+
+ @Test
+ public void hasLengthFail() {
+ expectFailureWhenTestingThat(array(2, 5)).hasLength(1);
+ assertFailureValue("value of", "array.length");
+ }
+
+ @Test
+ public void hasLengthNegative() {
+ try {
+ assertThat(array(2, 5)).hasLength(-1);
+ fail("Should have failed.");
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test
+ public void isEmpty() {
+ assertThat(EMPTY).isEmpty();
+ }
+
+ @Test
+ public void isEmptyFail() {
+ expectFailureWhenTestingThat(array(2, 5)).isEmpty();
+ assertFailureKeys("expected to be empty", "but was");
+ }
+
+ @Test
+ public void isNotEmpty() {
+ assertThat(array(2, 5)).isNotEmpty();
+ }
+
+ @Test
+ public void isNotEmptyFail() {
+ expectFailureWhenTestingThat(EMPTY).isNotEmpty();
+ assertFailureKeys("expected not to be empty");
+ }
+
+ @Test
+ public void isEqualTo_Fail_UnequalOrdering() {
+ expectFailureWhenTestingThat(array(2, 3)).isEqualTo(array(3, 2));
+ assertFailureKeys("expected", "but was", "differs at index");
+ assertFailureValue("expected", "[3, 2]");
+ assertFailureValue("but was", "[2, 3]");
+ assertFailureValue("differs at index", "[0]");
+ }
+
+ @Test
+ public void isEqualTo_Fail_NotAnArray() {
+ expectFailureWhenTestingThat(array(2, 3, 4)).isEqualTo(new Object());
+ }
+
+ @Test
+ public void isNotEqualTo_SameLengths() {
+ assertThat(array(2, 3)).isNotEqualTo(array(3, 2));
+ }
+
+ @Test
+ public void isNotEqualTo_DifferentLengths() {
+ assertThat(array(2, 3)).isNotEqualTo(array(2, 3, 1));
+ }
+
+ @Test
+ public void isNotEqualTo_DifferentTypes() {
+ assertThat(array(2, 3)).isNotEqualTo(new Object());
+ }
+
+ @Test
+ public void isNotEqualTo_FailEquals() {
+ expectFailureWhenTestingThat(array(2, 3)).isNotEqualTo(array(2, 3));
+ }
+
+ @SuppressWarnings("TruthSelfEquals")
+ @Test
+ public void isNotEqualTo_FailSame() {
+ int[] same = array(2, 3);
+ expectFailureWhenTestingThat(same).isNotEqualTo(same);
+ }
+
+ private static int[] array(int... ts) {
+ return ts;
+ }
+
+ private PrimitiveIntArraySubject expectFailureWhenTestingThat(int[] actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/PrimitiveLongArraySubjectTest.java b/core/src/test/java/com/google/common/truth/PrimitiveLongArraySubjectTest.java
new file mode 100644
index 00000000..b0795d84
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/PrimitiveLongArraySubjectTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link com.google.common.truth.PrimitiveLongArraySubject}.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+@RunWith(JUnit4.class)
+public class PrimitiveLongArraySubjectTest extends BaseSubjectTestCase {
+
+ @Test
+ public void isEqualTo() {
+ assertThat(array(2L, 5)).isEqualTo(array(2L, 5));
+ }
+
+ @SuppressWarnings("TruthSelfEquals")
+ @Test
+ public void isEqualTo_Same() {
+ long[] same = array(2L, 5);
+ assertThat(same).isEqualTo(same);
+ }
+
+ @Test
+ public void asList() {
+ assertThat(array(5, 2, 9)).asList().containsAtLeast(2L, 9L);
+ }
+
+ @Test
+ public void isEqualTo_Fail_UnequalOrdering() {
+ expectFailureWhenTestingThat(array(2, 3)).isEqualTo(array(3, 2));
+ assertFailureKeys("expected", "but was", "differs at index");
+ assertFailureValue("expected", "[3, 2]");
+ assertFailureValue("but was", "[2, 3]");
+ assertFailureValue("differs at index", "[0]");
+ }
+
+ @Test
+ public void isEqualTo_Fail_NotAnArray() {
+ expectFailureWhenTestingThat(array(2, 3, 4)).isEqualTo(new int[] {});
+ assertFailureKeys("expected", "but was", "wrong type", "expected", "but was");
+ assertFailureValueIndexed("expected", 1, "int[]");
+ assertFailureValueIndexed("but was", 1, "long[]");
+ }
+
+ @Test
+ public void isNotEqualTo_SameLengths() {
+ assertThat(array(2, 3)).isNotEqualTo(array(3, 2));
+ }
+
+ @Test
+ public void isNotEqualTo_DifferentLengths() {
+ assertThat(array(2, 3)).isNotEqualTo(array(2, 3, 1));
+ }
+
+ @Test
+ public void isNotEqualTo_DifferentTypes() {
+ assertThat(array(2, 3)).isNotEqualTo(new Object());
+ }
+
+ @Test
+ public void isNotEqualTo_FailEquals() {
+ expectFailureWhenTestingThat(array(2, 3)).isNotEqualTo(array(2, 3));
+ }
+
+ @SuppressWarnings("TruthSelfEquals")
+ @Test
+ public void isNotEqualTo_FailSame() {
+ long[] same = array(2, 3);
+ expectFailureWhenTestingThat(same).isNotEqualTo(same);
+ }
+
+ private static long[] array(long... ts) {
+ return ts;
+ }
+
+ private PrimitiveLongArraySubject expectFailureWhenTestingThat(long[] actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/PrimitiveShortArraySubjectTest.java b/core/src/test/java/com/google/common/truth/PrimitiveShortArraySubjectTest.java
new file mode 100644
index 00000000..4dad0ea4
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/PrimitiveShortArraySubjectTest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2015 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link PrimitiveShortArraySubject}.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+@RunWith(JUnit4.class)
+public class PrimitiveShortArraySubjectTest extends BaseSubjectTestCase {
+
+ @Test
+ public void isEqualTo() {
+ assertThat(array(1, 0, 1)).isEqualTo(array(1, 0, 1));
+ }
+
+ @SuppressWarnings("TruthSelfEquals")
+ @Test
+ public void isEqualTo_Same() {
+ short[] same = array(1, 0, 1);
+ assertThat(same).isEqualTo(same);
+ }
+
+ @Test
+ public void asList() {
+ assertThat(array(1, 1, 0)).asList().containsAtLeast((short) 1, (short) 0);
+ }
+
+ @Test
+ public void asListWithoutCastingFails() {
+ expectFailureWhenTestingThat(array(1, 1, 0)).asList().containsAtLeast(1, 0);
+ assertFailureKeys(
+ "value of",
+ "missing (2)",
+ "though it did contain (3)",
+ "---",
+ "expected to contain at least",
+ "but was");
+ }
+
+ @Test
+ public void isEqualTo_Fail_UnequalOrdering() {
+ expectFailureWhenTestingThat(array(1, 0, 1)).isEqualTo(array(0, 1, 1));
+ assertFailureKeys("expected", "but was", "differs at index");
+ assertFailureValue("expected", "[0, 1, 1]");
+ assertFailureValue("but was", "[1, 0, 1]");
+ assertFailureValue("differs at index", "[0]");
+ }
+
+ @Test
+ public void isEqualTo_Fail_NotAnArray() {
+ expectFailureWhenTestingThat(array(1, 0, 1)).isEqualTo(new Object());
+ }
+
+ @Test
+ public void isNotEqualTo_SameLengths() {
+ assertThat(array(1, 0)).isNotEqualTo(array(1, 1));
+ }
+
+ @Test
+ public void isNotEqualTo_DifferentLengths() {
+ assertThat(array(1, 0)).isNotEqualTo(array(1, 0, 1));
+ }
+
+ @Test
+ public void isNotEqualTo_DifferentTypes() {
+ assertThat(array(1, 0)).isNotEqualTo(new Object());
+ }
+
+ @Test
+ public void isNotEqualTo_FailEquals() {
+ expectFailureWhenTestingThat(array(1, 0)).isNotEqualTo(array(1, 0));
+ }
+
+ @SuppressWarnings("TruthSelfEquals")
+ @Test
+ public void isNotEqualTo_FailSame() {
+ short[] same = array(1, 0);
+ expectFailureWhenTestingThat(same).isNotEqualTo(same);
+ }
+
+ private static short[] array(int a, int b, int c) {
+ return new short[] {(short) a, (short) b, (short) c};
+ }
+
+ private static short[] array(int a, int b) {
+ return new short[] {(short) a, (short) b};
+ }
+
+ private PrimitiveShortArraySubject expectFailureWhenTestingThat(short[] actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/StackTraceCleanerTest.java b/core/src/test/java/com/google/common/truth/StackTraceCleanerTest.java
new file mode 100644
index 00000000..f42eee1f
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/StackTraceCleanerTest.java
@@ -0,0 +1,467 @@
+/*
+ * Copyright (c) 2017 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.ExpectFailure.expectFailure;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Range;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runner.Runner;
+import org.junit.runners.JUnit4;
+import org.junit.runners.model.Statement;
+
+/** Unit tests for {@link StackTraceCleaner}. */
+/*
+ * Cleaning doesn't actually work under j2cl (and presumably GWT): StackTraceElement.getClassName()
+ * doesn't have real data. Some data is available in toString(), albeit along the lines of
+ * "SimpleAssertionError.m_createError__java_lang_String_$pp_java_lang." StackTraceCleaner could
+ * maybe look through the toString() representations to count how many frames to remove, but that's
+ * a bigger project. (While we're at it, we could remove the j2cl-specific boilerplate from the
+ * _bottom_ of the stack, too.) And sadly, it's not necessarily as simple as looking at just _class_
+ * names: The cleaning is applied to causes, too, and it's possible for a cause to legitimately
+ * contain an exception created inside a class like Throwable -- e.g., x.initCause(x) will throw an
+ * exception, and it would be weird (though maybe tolerable) for us to remove that.
+ *
+ * Also note that j2cl includes some extra frames at the _top_, even beyond the ones that we try to
+ * remove: b/71355096
+ */
+@RunWith(JUnit4.class)
+public class StackTraceCleanerTest extends BaseSubjectTestCase {
+ @Test
+ public void realWorld() {
+ try {
+ assertThat(0).isEqualTo(1);
+ throw new Error();
+ } catch (AssertionError failure) {
+ assertThat(failure.getStackTrace()).hasLength(1);
+ }
+
+ // ExpectFailure ends up with "extra" frames, but that's probably the right behavior :\
+ AssertionError failure = expectFailure(whenTesting -> whenTesting.that(0).isEqualTo(1));
+ // Currently 3 total frames on the JVM, 4 on Android.
+ assertThat(failure.getStackTrace().length).isIn(Range.closed(3, 4));
+ }
+
+ @Test
+ public void emptyTrace() {
+ Throwable throwable = createThrowableWithStackTrace();
+
+ StackTraceCleaner.cleanStackTrace(throwable);
+
+ assertThat(throwable.getStackTrace()).isEqualTo(new StackTraceElement[0]);
+ }
+
+ @Test
+ public void collapseStreaks() {
+ Throwable throwable =
+ createThrowableWithStackTrace(
+ "com.example.MyTest",
+ "junit.Foo",
+ "org.junit.Bar",
+ "com.google.testing.junit.Car",
+ "com.google.testing.testsize.Dar",
+ "com.google.testing.util.Far",
+ "com.example.Gar");
+
+ StackTraceCleaner.cleanStackTrace(throwable);
+
+ assertThat(throwable.getStackTrace())
+ .isEqualTo(
+ new StackTraceElement[] {
+ createStackTraceElement("com.example.MyTest"),
+ createCollapsedStackTraceElement("Testing framework", 5),
+ createStackTraceElement("com.example.Gar"),
+ });
+ }
+
+ @Test
+ public void assertionsActuallyUseCleaner() {
+ expectFailure.whenTesting().that(1).isEqualTo(2);
+ assertThat(expectFailure.getFailure().getStackTrace()[0].getClassName())
+ .isEqualTo(getClass().getName());
+ }
+
+ @Test
+ public void assertionsActuallyUseCleaner_ComparisonFailure() {
+ expectFailure.whenTesting().that("1").isEqualTo("2");
+ assertThat(expectFailure.getFailure().getStackTrace()[0].getClassName())
+ .isEqualTo(getClass().getName());
+ }
+
+ @Test
+ public void dontCollapseStreaksOfOneFrame() {
+ Throwable throwable =
+ createThrowableWithStackTrace(
+ "com.example.MyTest",
+ "junit.Foo",
+ "com.example.Helper",
+ "org.junit.Bar",
+ "com.example.Helper",
+ "com.google.testing.junit.Car",
+ "com.google.testing.testsize.Dar",
+ "com.google.testing.util.Far",
+ "com.example.Gar");
+
+ StackTraceCleaner.cleanStackTrace(throwable);
+
+ assertThat(throwable.getStackTrace())
+ .isEqualTo(
+ new StackTraceElement[] {
+ createStackTraceElement("com.example.MyTest"),
+ createStackTraceElement("junit.Foo"),
+ createStackTraceElement("com.example.Helper"),
+ createStackTraceElement("org.junit.Bar"),
+ createStackTraceElement("com.example.Helper"),
+ createCollapsedStackTraceElement("Testing framework", 3),
+ createStackTraceElement("com.example.Gar"),
+ });
+ }
+
+ @Test
+ public void mixedStreaks() {
+ Throwable throwable =
+ createThrowableWithStackTrace(
+ "com.google.common.truth.IterableSubject",
+ "com.google.common.truth.MapSubject",
+ "com.example.MyTest",
+ "junit.Foo",
+ "org.junit.Bar",
+ "java.lang.reflect.Car",
+ "sun.reflect.Dar",
+ "com.google.testing.testsize.Dar",
+ "com.google.testing.util.Far",
+ "com.example.Jar");
+
+ StackTraceCleaner.cleanStackTrace(throwable);
+
+ assertThat(throwable.getStackTrace())
+ .isEqualTo(
+ new StackTraceElement[] {
+ createStackTraceElement("com.example.MyTest"),
+ createCollapsedStackTraceElement("Testing framework", 2),
+ createCollapsedStackTraceElement("Reflective call", 2),
+ createCollapsedStackTraceElement("Testing framework", 2),
+ createStackTraceElement("com.example.Jar"),
+ });
+ }
+
+ @Test
+ public void classNestedInSubject() {
+ Throwable throwable =
+ createThrowableWithStackTrace(
+ "com.google.common.truth.IterableSubject$UsingCorrespondence", "com.example.MyTest");
+
+ StackTraceCleaner.cleanStackTrace(throwable);
+
+ assertThat(throwable.getStackTrace())
+ .isEqualTo(
+ new StackTraceElement[] {
+ createStackTraceElement("com.example.MyTest"),
+ });
+ }
+
+ @Test
+ public void removesTestingAndReflectiveFramesOnBottom() {
+ Throwable throwable =
+ createThrowableWithStackTrace(
+ "com.example.Foo",
+ "com.example.Bar",
+ "sun.reflect.Car",
+ "org.junit.Dar",
+ "java.lang.reflect.Far",
+ "junit.Gar",
+ "com.google.testing.junit.Har",
+ "java.lang.reflect.Jar",
+ "java.lang.reflect.JarJar",
+ "com.google.testing.junit.Kar");
+
+ StackTraceCleaner.cleanStackTrace(throwable);
+
+ assertThat(throwable.getStackTrace())
+ .isEqualTo(
+ new StackTraceElement[] {
+ createStackTraceElement("com.example.Foo"),
+ createStackTraceElement("com.example.Bar"),
+ });
+ }
+
+ @Test
+ public void packagesAreIgnoredForTestClasses() {
+ Throwable throwable =
+ createThrowableWithStackTrace(
+ "com.google.testing.util.ShouldStrip1",
+ "com.google.testing.util.ShouldStrip2",
+ "com.google.testing.util.ShouldNotStripTest");
+
+ StackTraceCleaner.cleanStackTrace(throwable);
+
+ assertThat(throwable.getStackTrace())
+ .isEqualTo(
+ new StackTraceElement[] {
+ createCollapsedStackTraceElement("Testing framework", 2),
+ createStackTraceElement("com.google.testing.util.ShouldNotStripTest"),
+ });
+ }
+
+ @Test
+ public void allFramesAboveStandardSubjectBuilderCleaned() {
+ Throwable throwable =
+ createThrowableWithStackTrace(
+ "com.google.random.Package",
+ "com.google.common.base.collection.ImmutableMap",
+ "com.google.common.truth.StandardSubjectBuilder",
+ "com.google.example.SomeClass");
+
+ StackTraceCleaner.cleanStackTrace(throwable);
+
+ assertThat(throwable.getStackTrace())
+ .isEqualTo(
+ new StackTraceElement[] {
+ createStackTraceElement("com.google.example.SomeClass"),
+ });
+ }
+
+ @Test
+ public void allFramesAboveSubjectCleaned() {
+ Throwable throwable =
+ createThrowableWithStackTrace(
+ "com.google.random.Package",
+ "com.google.common.base.collection.ImmutableMap",
+ "com.google.common.truth.StringSubject",
+ "com.google.example.SomeClass");
+
+ StackTraceCleaner.cleanStackTrace(throwable);
+
+ assertThat(throwable.getStackTrace())
+ .isEqualTo(
+ new StackTraceElement[] {
+ createStackTraceElement("com.google.example.SomeClass"),
+ });
+ }
+
+ @Test
+ public void allFramesBelowJUnitStatementCleaned() {
+ Throwable throwable =
+ createThrowableWithStackTrace(
+ "com.google.common.truth.StringSubject",
+ "com.google.example.SomeTest",
+ SomeStatement.class.getName(),
+ "com.google.example.SomeClass");
+
+ StackTraceCleaner.cleanStackTrace(throwable);
+
+ assertThat(throwable.getStackTrace())
+ .isEqualTo(
+ new StackTraceElement[] {
+ createStackTraceElement("com.google.example.SomeTest"),
+ });
+ }
+
+ @Test
+ public void failureFromJUnitInfrastructureIncludesItInStack() {
+ Throwable throwable =
+ createThrowableWithStackTrace(
+ "com.google.common.truth.StringSubject",
+ SomeStatement.class.getName(),
+ "com.google.example.SomeClass");
+
+ StackTraceCleaner.cleanStackTrace(throwable);
+
+ assertThat(throwable.getStackTrace())
+ .isEqualTo(
+ new StackTraceElement[] {
+ createStackTraceElement(SomeStatement.class.getName()),
+ createStackTraceElement("com.google.example.SomeClass"),
+ });
+ }
+
+ @Test
+ public void allFramesBelowJUnitRunnerCleaned() {
+ Throwable throwable =
+ createThrowableWithStackTrace(
+ "com.google.common.truth.StringSubject",
+ "com.google.example.SomeTest",
+ SomeRunner.class.getName(),
+ "com.google.example.SomeClass");
+
+ StackTraceCleaner.cleanStackTrace(throwable);
+
+ assertThat(throwable.getStackTrace())
+ .isEqualTo(
+ new StackTraceElement[] {
+ createStackTraceElement("com.google.example.SomeTest"),
+ });
+ }
+
+ abstract static class SomeStatement extends Statement {}
+
+ abstract static class SomeRunner extends Runner {}
+
+ /**
+ * This scenario where truth class is called directly without any subject's subclass or {@link
+ * StandardSubjectBuilder} in the call stack should not happen in practical, testing anyway to
+ * make sure even if it does, the behavior should match expectation.
+ */
+ @Test
+ public void truthFrameWithOutSubject_shouldNotCleaned() {
+ Throwable throwable =
+ createThrowableWithStackTrace(
+ "com.google.random.Package",
+ // two or more truth frame will trigger string matching mechenism to got it collapsed
+ "com.google.common.truth.FailureMetadata",
+ "com.google.example.SomeClass");
+
+ StackTraceCleaner.cleanStackTrace(throwable);
+
+ assertThat(throwable.getStackTrace())
+ .isEqualTo(
+ new StackTraceElement[] {
+ createStackTraceElement("com.google.random.Package"),
+ createStackTraceElement("com.google.common.truth.FailureMetadata"),
+ createStackTraceElement("com.google.example.SomeClass"),
+ });
+ }
+
+ @Test
+ public void causingThrowablesAreAlsoCleaned() {
+ Throwable cause2 = createThrowableWithStackTrace("com.example.Foo", "org.junit.FilterMe");
+ Throwable cause1 =
+ createThrowableWithStackTrace(cause2, "com.example.Bar", "org.junit.FilterMe");
+ Throwable rootThrowable =
+ createThrowableWithStackTrace(cause1, "com.example.Car", "org.junit.FilterMe");
+
+ StackTraceCleaner.cleanStackTrace(rootThrowable);
+
+ assertThat(rootThrowable.getStackTrace()).isEqualTo(createStackTrace("com.example.Car"));
+ assertThat(cause1.getStackTrace()).isEqualTo(createStackTrace("com.example.Bar"));
+ assertThat(cause2.getStackTrace()).isEqualTo(createStackTrace("com.example.Foo"));
+ }
+
+ @Test
+ public void suppressedThrowablesAreAlsoCleaned() {
+ if (Platform.isAndroid()) {
+ return; // suppressed exceptions aren't supported under Ice Cream Sandwich, where we test
+ }
+ Throwable throwable = createThrowableWithStackTrace("com.example.Foo", "org.junit.FilterMe");
+ Throwable suppressed1 = createThrowableWithStackTrace("com.example.Bar", "org.junit.FilterMe");
+ Throwable suppressed2 = createThrowableWithStackTrace("com.example.Car", "org.junit.FilterMe");
+ throwable.addSuppressed(suppressed1);
+ throwable.addSuppressed(suppressed2);
+
+ StackTraceCleaner.cleanStackTrace(throwable);
+
+ assertThat(throwable.getStackTrace()).isEqualTo(createStackTrace("com.example.Foo"));
+ assertThat(suppressed1.getStackTrace()).isEqualTo(createStackTrace("com.example.Bar"));
+ assertThat(suppressed2.getStackTrace()).isEqualTo(createStackTrace("com.example.Car"));
+ }
+
+ @Test
+ public void mixedCausingAndSuppressThrowablesAreCleaned() {
+ if (Platform.isAndroid()) {
+ return; // suppressed exceptions aren't supported under Ice Cream Sandwich, where we test
+ }
+ Throwable suppressed1 = createThrowableWithStackTrace("com.example.Foo", "org.junit.FilterMe");
+ Throwable cause2 = createThrowableWithStackTrace("com.example.Bar", "org.junit.FilterMe");
+ Throwable cause1 =
+ createThrowableWithStackTrace(cause2, "com.example.Car", "org.junit.FilterMe");
+ Throwable suppressed2 =
+ createThrowableWithStackTrace(cause1, "com.example.Dar", "org.junit.FilterMe");
+ Throwable throwable = createThrowableWithStackTrace("com.example.Far", "org.junit.FilterMe");
+ throwable.addSuppressed(suppressed1);
+ throwable.addSuppressed(suppressed2);
+
+ StackTraceCleaner.cleanStackTrace(throwable);
+
+ assertThat(throwable.getStackTrace()).isEqualTo(createStackTrace("com.example.Far"));
+ assertThat(suppressed1.getStackTrace()).isEqualTo(createStackTrace("com.example.Foo"));
+ assertThat(suppressed2.getStackTrace()).isEqualTo(createStackTrace("com.example.Dar"));
+ assertThat(cause1.getStackTrace()).isEqualTo(createStackTrace("com.example.Car"));
+ assertThat(cause2.getStackTrace()).isEqualTo(createStackTrace("com.example.Bar"));
+ }
+
+ @Test
+ public void cleaningTraceIsIdempotent() {
+ Throwable throwable = createThrowableWithStackTrace("com.example.Foo", "org.junit.FilterMe");
+
+ StackTraceCleaner.cleanStackTrace(throwable);
+ StackTraceCleaner.cleanStackTrace(throwable);
+
+ assertThat(throwable.getStackTrace()).isEqualTo(createStackTrace("com.example.Foo"));
+ }
+
+ @Test
+ public void cyclesAreHandled() {
+ SelfReferencingThrowable selfReferencingThrowable =
+ new SelfReferencingThrowable("com.example.Foo", "org.junit.FilterMe");
+
+ StackTraceCleaner.cleanStackTrace(selfReferencingThrowable);
+
+ assertThat(selfReferencingThrowable.getStackTrace())
+ .isEqualTo(createStackTrace("com.example.Foo"));
+ }
+
+ private static Throwable createThrowableWithStackTrace(String... classNames) {
+ return createThrowableWithStackTrace(null, classNames);
+ }
+
+ private static Throwable createThrowableWithStackTrace(Throwable cause, String... classNames) {
+ Throwable throwable = new RuntimeException(cause);
+ StackTraceElement[] stackTrace = createStackTrace(classNames);
+ throwable.setStackTrace(stackTrace);
+ return throwable;
+ }
+
+ private static StackTraceElement[] createStackTrace(String... classNames) {
+ StackTraceElement[] stackTrace = new StackTraceElement[classNames.length];
+ for (int i = 0; i < classNames.length; i++) {
+ stackTrace[i] = createStackTraceElement(classNames[i]);
+ }
+ return stackTrace;
+ }
+
+ private static StackTraceElement createStackTraceElement(String className) {
+ return new StackTraceElement(className, "", "", -1);
+ }
+
+ private static StackTraceElement createCollapsedStackTraceElement(
+ String frameworkName, int collapsed) {
+ return new StackTraceElement(
+ "[["
+ + frameworkName
+ + ": "
+ + collapsed
+ + " frames collapsed ("
+ + StackTraceCleaner.CLEANER_LINK
+ + ")]]",
+ "",
+ "",
+ 0);
+ }
+
+ private static class SelfReferencingThrowable extends Throwable {
+
+ SelfReferencingThrowable(String... classNames) {
+ setStackTrace(createStackTrace(classNames));
+ }
+
+ @Override
+ public synchronized Throwable getCause() {
+ return this;
+ }
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/StandardSubjectBuilderTest.java b/core/src/test/java/com/google/common/truth/StandardSubjectBuilderTest.java
new file mode 100644
index 00000000..f6751209
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/StandardSubjectBuilderTest.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2019 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link StandardSubjectBuilder}. */
+@RunWith(JUnit4.class)
+public final class StandardSubjectBuilderTest extends BaseSubjectTestCase {
+ @Test
+ public void failNoMessage() {
+ expectFailure.whenTesting().fail();
+ assertThatFailure().hasMessageThat().isEmpty();
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/StringSubjectTest.java b/core/src/test/java/com/google/common/truth/StringSubjectTest.java
new file mode 100644
index 00000000..dbb89112
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/StringSubjectTest.java
@@ -0,0 +1,550 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.ExpectFailure.assertThat;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.annotations.GwtIncompatible;
+import java.util.regex.Pattern;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for String Subjects.
+ *
+ * @author David Saff
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+@RunWith(JUnit4.class)
+public class StringSubjectTest extends BaseSubjectTestCase {
+
+ @Test
+ public void hasLength() {
+ assertThat("kurt").hasLength(4);
+ }
+
+ @Test
+ public void hasLengthZero() {
+ assertThat("").hasLength(0);
+ }
+
+ @Test
+ public void hasLengthFails() {
+ expectFailureWhenTestingThat("kurt").hasLength(5);
+ assertFailureValue("value of", "string.length()");
+ }
+
+ @Test
+ public void hasLengthNegative() {
+ try {
+ assertThat("kurt").hasLength(-1);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test
+ public void stringIsEmpty() {
+ assertThat("").isEmpty();
+ }
+
+ @Test
+ public void stringIsEmptyFail() {
+ expectFailureWhenTestingThat("abc").isEmpty();
+ assertFailureKeys("expected to be empty", "but was");
+ }
+
+ @Test
+ public void stringIsEmptyFailNull() {
+ expectFailureWhenTestingThat(null).isEmpty();
+ assertFailureKeys("expected empty string", "but was");
+ }
+
+ @Test
+ public void stringIsNotEmpty() {
+ assertThat("abc").isNotEmpty();
+ }
+
+ @Test
+ public void stringIsNotEmptyFail() {
+ expectFailureWhenTestingThat("").isNotEmpty();
+ assertFailureKeys("expected not to be empty");
+ }
+
+ @Test
+ public void stringIsNotEmptyFailNull() {
+ expectFailureWhenTestingThat(null).isNotEmpty();
+ assertFailureKeys("expected nonempty string", "but was");
+ }
+
+ @Test
+ public void stringContains() {
+ assertThat("abc").contains("c");
+ }
+
+ @Test
+ public void stringContainsCharSeq() {
+ CharSequence charSeq = new StringBuilder("c");
+ assertThat("abc").contains(charSeq);
+ }
+
+ @Test
+ public void stringContainsFail() {
+ expectFailureWhenTestingThat("abc").contains("d");
+ assertFailureValue("expected to contain", "d");
+ }
+
+ @Test
+ public void stringDoesNotContain() {
+ assertThat("abc").doesNotContain("d");
+ }
+
+ @Test
+ public void stringDoesNotContainCharSequence() {
+ CharSequence charSeq = new StringBuilder("d");
+ assertThat("abc").doesNotContain(charSeq);
+ }
+
+ @Test
+ public void stringDoesNotContainFail() {
+ expectFailureWhenTestingThat("abc").doesNotContain("b");
+ assertFailureValue("expected not to contain", "b");
+ }
+
+ @Test
+ public void stringEquality() {
+ assertThat("abc").isEqualTo("abc");
+ }
+
+ @Test
+ public void stringEqualityToNull() {
+ expectFailureWhenTestingThat("abc").isEqualTo(null);
+ assertThat(expectFailure.getFailure()).isNotInstanceOf(ComparisonFailureWithFacts.class);
+ }
+
+ @Test
+ public void stringEqualityToEmpty() {
+ expectFailureWhenTestingThat("abc").isEqualTo("");
+ assertFailureKeys("expected an empty string", "but was");
+ }
+
+ @Test
+ public void stringEqualityEmptyToNonEmpty() {
+ expectFailureWhenTestingThat("").isEqualTo("abc");
+ assertFailureKeys("expected", "but was an empty string");
+ }
+
+ @Test
+ public void stringEqualityFail() {
+ expectFailureWhenTestingThat("abc").isEqualTo("ABC");
+ assertThat(expectFailure.getFailure()).isInstanceOf(ComparisonFailureWithFacts.class);
+ }
+
+ @Test
+ public void stringStartsWith() {
+ assertThat("abc").startsWith("ab");
+ }
+
+ @Test
+ public void stringStartsWithFail() {
+ expectFailureWhenTestingThat("abc").startsWith("bc");
+ assertFailureValue("expected to start with", "bc");
+ }
+
+ @Test
+ public void stringEndsWith() {
+ assertThat("abc").endsWith("bc");
+ }
+
+ @Test
+ public void stringEndsWithFail() {
+ expectFailureWhenTestingThat("abc").endsWith("ab");
+ assertFailureValue("expected to end with", "ab");
+ }
+
+ @Test
+ public void emptyStringTests() {
+ assertThat("").contains("");
+ assertThat("").startsWith("");
+ assertThat("").endsWith("");
+ assertThat("a").contains("");
+ assertThat("a").startsWith("");
+ assertThat("a").endsWith("");
+ }
+
+ @Test
+ public void stringMatchesString() {
+ assertThat("abcaaadev").matches(".*aaa.*");
+ }
+
+ @Test
+ public void stringMatchesStringWithFail() {
+ expectFailureWhenTestingThat("abcaqadev").matches(".*aaa.*");
+ assertFailureValue("expected to match", ".*aaa.*");
+ }
+
+ @Test
+ public void stringMatchesStringFailNull() {
+ expectFailureWhenTestingThat(null).matches(".*aaa.*");
+ assertFailureValue("expected a string that matches", ".*aaa.*");
+ }
+
+ @Test
+ public void stringMatchesStringLiteralFail() {
+ expectFailureWhenTestingThat("$abc").matches("$abc");
+ assertFailureValue("expected to match", "$abc");
+ assertFailureValue("but was", "$abc");
+ assertThat(expectFailure.getFailure())
+ .factKeys()
+ .contains("Looks like you want to use .isEqualTo() for an exact equality assertion.");
+ }
+
+ @Test
+ @GwtIncompatible("Pattern")
+ public void stringMatchesPattern() {
+ assertThat("abcaaadev").matches(Pattern.compile(".*aaa.*"));
+ }
+
+ @Test
+ @GwtIncompatible("Pattern")
+ public void stringMatchesPatternWithFail() {
+ expectFailureWhenTestingThat("abcaqadev").matches(Pattern.compile(".*aaa.*"));
+ assertFailureValue("expected to match", ".*aaa.*");
+ }
+
+ @Test
+ @GwtIncompatible("Pattern")
+ public void stringMatchesPatternFailNull() {
+ expectFailureWhenTestingThat(null).matches(Pattern.compile(".*aaa.*"));
+ assertFailureValue("expected a string that matches", ".*aaa.*");
+ }
+
+ @Test
+ @GwtIncompatible("Pattern")
+ public void stringMatchesPatternLiteralFail() {
+ expectFailureWhenTestingThat("$abc").matches(Pattern.compile("$abc"));
+ assertFailureValue("expected to match", "$abc");
+ assertFailureValue("but was", "$abc");
+ assertThat(expectFailure.getFailure())
+ .factKeys()
+ .contains(
+ "If you want an exact equality assertion you can escape your regex with"
+ + " Pattern.quote().");
+ }
+
+ @Test
+ public void stringDoesNotMatchString() {
+ assertThat("abcaqadev").doesNotMatch(".*aaa.*");
+ }
+
+ @Test
+ public void stringDoesNotMatchStringWithFail() {
+ expectFailureWhenTestingThat("abcaaadev").doesNotMatch(".*aaa.*");
+ assertFailureValue("expected not to match", ".*aaa.*");
+ }
+
+ @Test
+ public void stringDoesNotMatchStringFailNull() {
+ expectFailureWhenTestingThat(null).doesNotMatch(".*aaa.*");
+ assertFailureValue("expected a string that does not match", ".*aaa.*");
+ }
+
+ @Test
+ @GwtIncompatible("Pattern")
+ public void stringDoesNotMatchPattern() {
+ assertThat("abcaqadev").doesNotMatch(Pattern.compile(".*aaa.*"));
+ }
+
+ @Test
+ @GwtIncompatible("Pattern")
+ public void stringDoesNotMatchPatternWithFail() {
+ expectFailureWhenTestingThat("abcaaadev").doesNotMatch(Pattern.compile(".*aaa.*"));
+ assertFailureValue("expected not to match", ".*aaa.*");
+ }
+
+ @Test
+ @GwtIncompatible("Pattern")
+ public void stringDoesNotMatchPatternFailNull() {
+ expectFailureWhenTestingThat(null).doesNotMatch(Pattern.compile(".*aaa.*"));
+ assertFailureValue("expected a string that does not match", ".*aaa.*");
+ }
+
+ @Test
+ @GwtIncompatible("Pattern")
+ public void stringContainsMatchStringUsesFind() {
+ assertThat("aba").containsMatch("[b]");
+ assertThat("aba").containsMatch(Pattern.compile("[b]"));
+ }
+
+ @Test
+ public void stringContainsMatchString() {
+ assertThat("aba").containsMatch(".*b.*");
+
+ expectFailureWhenTestingThat("aaa").containsMatch(".*b.*");
+ assertFailureValue("expected to contain a match for", ".*b.*");
+ }
+
+ @Test
+ public void stringContainsMatchStringFailNull() {
+ expectFailureWhenTestingThat(null).containsMatch(".*b.*");
+ assertFailureValue("expected a string that contains a match for", ".*b.*");
+ }
+
+ @Test
+ @GwtIncompatible("Pattern")
+ public void stringContainsMatchPattern() {
+ assertThat("aba").containsMatch(Pattern.compile(".*b.*"));
+
+ expectFailureWhenTestingThat("aaa").containsMatch(Pattern.compile(".*b.*"));
+ assertFailureValue("expected to contain a match for", ".*b.*");
+ }
+
+ @Test
+ @GwtIncompatible("Pattern")
+ public void stringContainsMatchPatternFailNull() {
+ expectFailureWhenTestingThat(null).containsMatch(Pattern.compile(".*b.*"));
+ assertFailureValue("expected a string that contains a match for", ".*b.*");
+ }
+
+ @Test
+ public void stringDoesNotContainMatchString() {
+ assertThat("aaa").doesNotContainMatch(".*b.*");
+
+ expectFailureWhenTestingThat("aba").doesNotContainMatch(".*b.*");
+ assertFailureValue("expected not to contain a match for", ".*b.*");
+ }
+
+ @Test
+ public void stringDoesNotContainMatchStringUsesFind() {
+ expectFailureWhenTestingThat("aba").doesNotContainMatch("[b]");
+ assertFailureValue("expected not to contain a match for", "[b]");
+ }
+
+ @Test
+ public void stringDoesNotContainMatchStringUsesFindFailNull() {
+ expectFailureWhenTestingThat(null).doesNotContainMatch("[b]");
+ assertFailureValue("expected a string that does not contain a match for", "[b]");
+ }
+
+ @Test
+ @GwtIncompatible("Pattern")
+ public void stringDoesNotContainMatchPattern() {
+ assertThat("zzaaazz").doesNotContainMatch(Pattern.compile(".b."));
+
+ expectFailureWhenTestingThat("zzabazz").doesNotContainMatch(Pattern.compile(".b."));
+ assertFailureValue("expected not to contain a match for", ".b.");
+ assertFailureValue("but contained", "aba");
+ assertFailureValue("full string", "zzabazz");
+ }
+
+ @Test
+ @GwtIncompatible("Pattern")
+ public void stringDoesNotContainMatchPatternFailNull() {
+ expectFailureWhenTestingThat(null).doesNotContainMatch(Pattern.compile(".b."));
+ assertFailureValue("expected a string that does not contain a match for", ".b.");
+ }
+
+ @Test
+ public void stringEqualityIgnoringCase() {
+ assertThat("cafĂ©").ignoringCase().isEqualTo("CAFÉ");
+ }
+
+ @Test
+ public void stringEqualityIgnoringCaseWithNullSubject() {
+ assertThat((String) null).ignoringCase().isEqualTo(null);
+ }
+
+ @Test
+ public void stringEqualityIgnoringCaseFail() {
+ expectFailureWhenTestingThat("abc").ignoringCase().isEqualTo("abd");
+
+ assertFailureValue("expected", "abd");
+ assertThat(expectFailure.getFailure()).factKeys().contains("(case is ignored)");
+ }
+
+ @Test
+ public void stringEqualityIgnoringCaseFailWithNullSubject() {
+ expectFailureWhenTestingThat((String) null).ignoringCase().isEqualTo("abc");
+
+ assertFailureValue("expected a string that is equal to", "abc");
+ assertThat(expectFailure.getFailure()).factKeys().contains("(case is ignored)");
+ }
+
+ @Test
+ public void stringEqualityIgnoringCaseFailWithNullExpectedString() {
+ expectFailureWhenTestingThat("abc").ignoringCase().isEqualTo(null);
+
+ assertFailureValue("expected", "null (null reference)");
+ assertThat(expectFailure.getFailure()).factKeys().contains("(case is ignored)");
+ }
+
+ @Test
+ public void stringInequalityIgnoringCase() {
+ assertThat("cafĂ©").ignoringCase().isNotEqualTo("AFÉ");
+ }
+
+ @Test
+ public void stringInequalityIgnoringCaseWithNullSubject() {
+ assertThat((String) null).ignoringCase().isNotEqualTo("abc");
+ }
+
+ @Test
+ public void stringInequalityIgnoringCaseWithNullExpectedString() {
+ assertThat("abc").ignoringCase().isNotEqualTo(null);
+ }
+
+ @Test
+ public void stringInequalityIgnoringCaseFail() {
+ expectFailureWhenTestingThat("cafĂ©").ignoringCase().isNotEqualTo("CAFÉ");
+
+ assertFailureValue("expected not to be", "CAFÉ");
+ assertThat(expectFailure.getFailure()).factKeys().contains("(case is ignored)");
+ }
+
+ @Test
+ public void stringInequalityIgnoringCaseFailWithNullSubject() {
+ expectFailureWhenTestingThat((String) null).ignoringCase().isNotEqualTo(null);
+
+ assertFailureValue("expected a string that is not equal to", "null (null reference)");
+ assertThat(expectFailure.getFailure()).factKeys().contains("(case is ignored)");
+ }
+
+ @Test
+ public void stringContainsIgnoringCase() {
+ assertThat("Ă€bc").ignoringCase().contains("Ä");
+ }
+
+ @Test
+ public void stringContainsIgnoringCaseEmptyString() {
+ assertThat("abc").ignoringCase().contains("");
+ }
+
+ @Test
+ public void stringContainsIgnoringCaseWithWord() {
+ assertThat("abcdĂ©").ignoringCase().contains("CdÉ");
+ }
+
+ @Test
+ public void stringContainsIgnoringCaseWholeWord() {
+ assertThat("abcde").ignoringCase().contains("ABCde");
+ }
+
+ @Test
+ public void stringContainsIgnoringCaseCharSeq() {
+ CharSequence charSeq = new StringBuilder("C");
+ assertThat("abc").ignoringCase().contains(charSeq);
+ }
+
+ @Test
+ public void stringContainsIgnoringCaseFail() {
+ expectFailureWhenTestingThat("abc").ignoringCase().contains("d");
+
+ assertFailureValue("expected to contain", "d");
+ assertThat(expectFailure.getFailure()).factKeys().contains("(case is ignored)");
+ }
+
+ @Test
+ public void stringContainsIgnoringCaseFailBecauseTooLarge() {
+ expectFailureWhenTestingThat("abc").ignoringCase().contains("abcc");
+
+ assertFailureValue("expected to contain", "abcc");
+ assertThat(expectFailure.getFailure()).factKeys().contains("(case is ignored)");
+ }
+
+ @Test
+ public void stringContainsIgnoringCaseFailBecauseNullSubject() {
+ expectFailureWhenTestingThat((String) null).ignoringCase().contains("d");
+
+ assertFailureValue("expected a string that contains", "d");
+ assertThat(expectFailure.getFailure()).factKeys().contains("(case is ignored)");
+ }
+
+ @Test
+ public void stringDoesNotContainIgnoringCase() {
+ assertThat("Ă€bc").ignoringCase().doesNotContain("Äc");
+ }
+
+ @Test
+ public void stringDoesNotContainIgnoringCaseCharSeq() {
+ CharSequence charSeq = new StringBuilder("cb");
+ assertThat("abc").ignoringCase().doesNotContain(charSeq);
+ }
+
+ @Test
+ public void stringDoesNotContainIgnoringCaseFail() {
+ expectFailureWhenTestingThat("Ă€bc").ignoringCase().doesNotContain("Äb");
+
+ assertFailureValue("expected not to contain", "Äb");
+ assertThat(expectFailure.getFailure()).factKeys().contains("(case is ignored)");
+ }
+
+ @Test
+ public void stringDoesNotContainIgnoringCaseFailWithEmptyString() {
+ expectFailureWhenTestingThat("abc").ignoringCase().doesNotContain("");
+
+ assertFailureValue("expected not to contain", "");
+ assertThat(expectFailure.getFailure()).factKeys().contains("(case is ignored)");
+ }
+
+ @Test
+ public void stringDoesNotContainIgnoringCaseFailBecauseNullSubject() {
+ expectFailureWhenTestingThat((String) null).ignoringCase().doesNotContain("d");
+
+ assertFailureValue("expected a string that does not contain", "d");
+ assertThat(expectFailure.getFailure()).factKeys().contains("(case is ignored)");
+ }
+
+ @Test
+ public void trailingWhitespaceInActual() {
+ expectFailureWhenTestingThat("foo\n").isEqualTo("foo");
+ assertFailureKeys("expected", "but contained extra trailing whitespace");
+ assertFailureValue("but contained extra trailing whitespace", "\\n");
+ }
+
+ @Test
+ public void trailingWhitespaceInExpected() {
+ expectFailureWhenTestingThat("foo").isEqualTo("foo ");
+ assertFailureKeys("expected", "but was missing trailing whitespace");
+ assertFailureValue("but was missing trailing whitespace", "␣");
+ }
+
+ @Test
+ public void trailingWhitespaceInBoth() {
+ expectFailureWhenTestingThat("foo \n").isEqualTo("foo\u00a0");
+ assertFailureKeys("expected", "with trailing whitespace", "but trailing whitespace was");
+ assertFailureValue("with trailing whitespace", "\\u00a0");
+ assertFailureValue("but trailing whitespace was", "␣\\n");
+ }
+
+ @Test
+ public void trailingWhitespaceVsEmptyString() {
+ /*
+ * The code has special cases for both trailing whitespace and an empty string. Make sure that
+ * it specifically reports the trailing whitespace. (It might be nice to *also* report the empty
+ * string specially, but that's less important.)
+ */
+ expectFailureWhenTestingThat("\t").isEqualTo("");
+ assertFailureKeys("expected", "but contained extra trailing whitespace");
+ assertFailureValue("but contained extra trailing whitespace", "\\t");
+ }
+
+ private StringSubject expectFailureWhenTestingThat(String actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/SubjectTest.java b/core/src/test/java/com/google/common/truth/SubjectTest.java
new file mode 100644
index 00000000..32919ce4
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/SubjectTest.java
@@ -0,0 +1,814 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.truth.ExpectFailure.assertThat;
+import static com.google.common.truth.Fact.simpleFact;
+import static com.google.common.truth.TestPlatform.isGwt;
+import static com.google.common.truth.Truth.assertAbout;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.annotations.GwtIncompatible;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableMultiset;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.ImmutableTable;
+import com.google.common.collect.Iterators;
+import com.google.common.primitives.UnsignedInteger;
+import com.google.common.testing.NullPointerTester;
+import com.google.common.truth.Subject.Factory;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.Iterator;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for generic Subject behavior.
+ *
+ * @author David Saff
+ * @author Christian Gruber
+ */
+@RunWith(JUnit4.class)
+public class SubjectTest extends BaseSubjectTestCase {
+
+ @Test
+ @GwtIncompatible("NullPointerTester")
+ @SuppressWarnings("GoogleInternalApi")
+ public void nullPointerTester() {
+ NullPointerTester npTester = new NullPointerTester();
+ npTester.setDefault(Fact.class, simpleFact("fact"));
+
+ // TODO(kak): Automatically generate this list with reflection,
+ // or maybe use AbstractPackageSanityTests?
+ npTester.testAllPublicInstanceMethods(assertThat(BigDecimal.TEN));
+ npTester.testAllPublicInstanceMethods(assertThat(false));
+ npTester.testAllPublicInstanceMethods(assertThat(String.class));
+ npTester.testAllPublicInstanceMethods(assertThat((Comparable<String>) "hello"));
+ npTester.testAllPublicInstanceMethods(assertThat(2d));
+ npTester.testAllPublicInstanceMethods(assertThat(2f));
+ npTester.testAllPublicInstanceMethods(assertThat(Optional.absent()));
+ npTester.testAllPublicInstanceMethods(assertThat(1));
+ npTester.testAllPublicInstanceMethods(assertThat(ImmutableList.of()));
+ npTester.testAllPublicInstanceMethods(assertThat(ImmutableListMultimap.of()));
+ npTester.testAllPublicInstanceMethods(assertThat(1L));
+ npTester.testAllPublicInstanceMethods(assertThat(ImmutableMap.of()));
+ npTester.testAllPublicInstanceMethods(assertThat(ImmutableMultimap.of()));
+ npTester.testAllPublicInstanceMethods(assertThat(ImmutableMultiset.of()));
+ npTester.testAllPublicInstanceMethods(assertThat(new Object[0]));
+ npTester.testAllPublicInstanceMethods(assertThat(ImmutableSetMultimap.of()));
+ npTester.testAllPublicInstanceMethods(assertThat("hello"));
+ npTester.testAllPublicInstanceMethods(assertThat(new Object()));
+ npTester.testAllPublicInstanceMethods(assertThat(ImmutableTable.of()));
+ npTester.testAllPublicInstanceMethods(assertThat(new Exception()));
+ }
+
+ @Test
+ @GwtIncompatible("NullPointerTester")
+ public void allAssertThatOverloadsAcceptNull() throws Exception {
+ NullPointerTester npTester = new NullPointerTester();
+ npTester.setDefault(Fact.class, simpleFact("fact"));
+ for (Method method : Truth.class.getDeclaredMethods()) {
+ if (Modifier.isPublic(method.getModifiers())
+ && method.getName().equals("assertThat")
+ && method.getParameterTypes().length == 1) {
+ Object actual = null;
+ Subject subject = (Subject) method.invoke(Truth.class, actual);
+
+ subject.isNull();
+ try {
+ subject.isNotNull(); // should throw
+ throw new Error("assertThat(null).isNotNull() should throw an exception!");
+ } catch (AssertionError expected) {
+ assertThat(expected).factKeys().containsExactly("expected not to be");
+ assertThat(expected).factValue("expected not to be").isEqualTo("null");
+ }
+
+ subject.isEqualTo(null);
+ try {
+ subject.isNotEqualTo(null); // should throw
+ throw new Error("assertThat(null).isNotEqualTo(null) should throw an exception!");
+ } catch (AssertionError expected) {
+ }
+
+ subject.isSameInstanceAs(null);
+ subject.isNotSameInstanceAs(new Object());
+
+ if (!(subject instanceof IterableSubject)) { // b/36000148
+ subject.isNotIn(ImmutableList.<Object>of());
+ subject.isNoneOf(new Object(), new Object());
+ }
+
+ try {
+ subject.isIn(ImmutableList.of());
+ throw new Error("Expected to fail");
+ } catch (AssertionError expected) {
+ assertThat(expected).factKeys().contains("expected any of");
+ }
+
+ // TODO(cpovirk): Fix bug.
+ if (!(subject instanceof AbstractArraySubject)) {
+ // check all public assertion methods for correct null handling
+ npTester.testAllPublicInstanceMethods(subject);
+ }
+
+ subject.isNotEqualTo(new Object());
+ subject.isEqualTo(null);
+ try {
+ subject.isEqualTo(new Object()); // should throw
+ throw new Error("assertThat(null).isEqualTo(<non-null>) should throw an exception!");
+ } catch (AssertionError expected) {
+ assertThat(expected).factKeys().containsExactly("expected", "but was").inOrder();
+ }
+ }
+ }
+ }
+
+ private static final Object OBJECT_1 =
+ new Object() {
+ @Override
+ public String toString() {
+ return "Object 1";
+ }
+ };
+ private static final Object OBJECT_2 =
+ new Object() {
+ @Override
+ public String toString() {
+ return "Object 2";
+ }
+ };
+
+ @SuppressWarnings("TruthIncompatibleType") // Intentional for testing purposes.
+ @Test
+ public void toStringsAreIdentical() {
+ IntWrapper wrapper = new IntWrapper();
+ wrapper.wrapped = 5;
+ expectFailure.whenTesting().that(5).isEqualTo(wrapper);
+ assertFailureKeys("expected", "an instance of", "but was", "an instance of");
+ assertFailureValue("expected", "5");
+ assertFailureValueIndexed(
+ "an instance of", 0, "com.google.common.truth.SubjectTest$IntWrapper");
+ assertFailureValue("but was", "(non-equal value with same string representation)");
+ assertFailureValueIndexed("an instance of", 1, "java.lang.Integer");
+ }
+
+ private static class IntWrapper {
+ int wrapped;
+
+ @Override
+ public String toString() {
+ return Integer.toString(wrapped);
+ }
+ }
+
+ @Test
+ public void isSameInstanceAsWithNulls() {
+ Object o = null;
+ assertThat(o).isSameInstanceAs(null);
+ }
+
+ @Test
+ public void isSameInstanceAsFailureWithNulls() {
+ Object o = null;
+ expectFailure.whenTesting().that(o).isSameInstanceAs("a");
+ assertFailureKeys("expected specific instance", "but was");
+ assertFailureValue("expected specific instance", "a");
+ }
+
+ @Test
+ public void isSameInstanceAsWithSameObject() {
+ Object a = new Object();
+ Object b = a;
+ assertThat(a).isSameInstanceAs(b);
+ }
+
+ @Test
+ public void isSameInstanceAsFailureWithObjects() {
+ Object a = OBJECT_1;
+ Object b = OBJECT_2;
+ expectFailure.whenTesting().that(a).isSameInstanceAs(b);
+ assertThat(expectFailure.getFailure()).isNotInstanceOf(ComparisonFailureWithFacts.class);
+ }
+
+ @Test
+ public void isSameInstanceAsFailureWithComparableObjects_nonString() {
+ Object a = UnsignedInteger.valueOf(42);
+ Object b = UnsignedInteger.fromIntBits(42);
+ expectFailure.whenTesting().that(a).isSameInstanceAs(b);
+ assertFailureKeys("expected specific instance", "but was");
+ assertFailureValue("expected specific instance", "42");
+ assertFailureValue(
+ "but was", "(different but equal instance of same class with same string representation)");
+ }
+
+ @Test
+ @GwtIncompatible("String equality under JS")
+ public void isSameInstanceAsFailureWithComparableObjects() {
+ Object a = "ab";
+ Object b = new StringBuilder("ab").toString();
+ expectFailure.whenTesting().that(a).isSameInstanceAs(b);
+ }
+
+ @Test
+ public void isSameInstanceAsFailureWithDifferentTypesAndSameToString() {
+ Object a = "true";
+ Object b = true;
+ expectFailure.whenTesting().that(a).isSameInstanceAs(b);
+ assertFailureKeys("expected specific instance", "an instance of", "but was", "an instance of");
+ assertFailureValue("expected specific instance", "true");
+ assertFailureValueIndexed("an instance of", 0, "java.lang.Boolean");
+ assertFailureValue("but was", "(non-equal value with same string representation)");
+ assertFailureValueIndexed("an instance of", 1, "java.lang.String");
+ }
+
+ @Test
+ public void isNotSameInstanceAsWithNulls() {
+ Object o = null;
+ assertThat(o).isNotSameInstanceAs("a");
+ }
+
+ @Test
+ public void isNotSameInstanceAsFailureWithNulls() {
+ Object o = null;
+ expectFailure.whenTesting().that(o).isNotSameInstanceAs(null);
+ assertFailureKeys("expected not to be specific instance");
+ assertFailureValue("expected not to be specific instance", "null");
+ }
+
+ @Test
+ public void isNotSameInstanceAsWithObjects() {
+ Object a = new Object();
+ Object b = new Object();
+ assertThat(a).isNotSameInstanceAs(b);
+ }
+
+ @Test
+ public void isNotSameInstanceAsFailureWithSameObject() {
+ Object a = OBJECT_1;
+ Object b = a;
+ expectFailure.whenTesting().that(a).isNotSameInstanceAs(b);
+ assertFailureKeys("expected not to be specific instance");
+ assertFailureValue("expected not to be specific instance", "Object 1");
+ }
+
+ @Test
+ public void isNotSameInstanceAsWithComparableObjects_nonString() {
+ Object a = UnsignedInteger.valueOf(42);
+ Object b = UnsignedInteger.fromIntBits(42);
+ assertThat(a).isNotSameInstanceAs(b);
+ }
+
+ @Test
+ @GwtIncompatible("String equality under JS")
+ public void isNotSameInstanceAsWithComparableObjects() {
+ Object a = "ab";
+ Object b = new StringBuilder("ab").toString();
+ assertThat(a).isNotSameInstanceAs(b);
+ }
+
+ @Test
+ public void isNotSameInstanceAsWithDifferentTypesAndSameToString() {
+ Object a = "true";
+ Object b = true;
+ assertThat(a).isNotSameInstanceAs(b);
+ }
+
+ @Test
+ public void isNull() {
+ Object o = null;
+ assertThat(o).isNull();
+ }
+
+ @Test
+ public void isNullFail() {
+ Object o = new Object();
+ expectFailure.whenTesting().that(o).isNull();
+ assertFailureKeys("expected", "but was");
+ assertFailureValue("expected", "null");
+ }
+
+ @Test
+ public void isNullWhenSubjectForbidsIsEqualTo() {
+ assertAbout(objectsForbiddingEqualityCheck()).that(null).isNull();
+ }
+
+ @Test
+ public void isNullWhenSubjectForbidsIsEqualToFail() {
+ expectFailure.whenTesting().about(objectsForbiddingEqualityCheck()).that(new Object()).isNull();
+ }
+
+ @Test
+ public void stringIsNullFail() {
+ expectFailure.whenTesting().that("foo").isNull();
+ }
+
+ @Test
+ public void isNullBadEqualsImplementation() {
+ expectFailure.whenTesting().that(new ThrowsOnEqualsNull()).isNull();
+ }
+
+ @Test
+ public void isNotNull() {
+ Object o = new Object();
+ assertThat(o).isNotNull();
+ }
+
+ @Test
+ public void isNotNullFail() {
+ Object o = null;
+ expectFailure.whenTesting().that(o).isNotNull();
+ assertFailureKeys("expected not to be");
+ assertFailureValue("expected not to be", "null");
+ }
+
+ @Test
+ public void isNotNullBadEqualsImplementation() {
+ assertThat(new ThrowsOnEqualsNull()).isNotNull();
+ }
+
+ @Test
+ public void isNotNullWhenSubjectForbidsIsEqualTo() {
+ assertAbout(objectsForbiddingEqualityCheck()).that(new Object()).isNotNull();
+ }
+
+ @Test
+ public void isNotNullWhenSubjectForbidsIsEqualToFail() {
+ expectFailure.whenTesting().about(objectsForbiddingEqualityCheck()).that(null).isNotNull();
+ }
+
+ @Test
+ public void isEqualToWithNulls() {
+ Object o = null;
+ assertThat(o).isEqualTo(null);
+ }
+
+ @Test
+ public void isEqualToFailureWithNulls() {
+ Object o = null;
+ expectFailure.whenTesting().that(o).isEqualTo("a");
+ assertFailureKeys("expected", "but was");
+ assertFailureValue("expected", "a");
+ assertFailureValue("but was", "null");
+ }
+
+ @Test
+ public void isEqualToStringWithNullVsNull() {
+ expectFailure.whenTesting().that("null").isEqualTo(null);
+ assertFailureKeys("expected", "an instance of", "but was", "an instance of");
+ assertFailureValue("expected", "null");
+ assertFailureValueIndexed("an instance of", 0, "(null reference)");
+ assertFailureValue("but was", "(non-equal value with same string representation)");
+ assertFailureValueIndexed("an instance of", 1, "java.lang.String");
+ }
+
+ @Test
+ public void isEqualToWithSameObject() {
+ Object a = new Object();
+ Object b = a;
+ assertThat(a).isEqualTo(b);
+ }
+
+ @Test
+ public void isEqualToFailureWithObjects() {
+ Object a = OBJECT_1;
+ Object b = OBJECT_2;
+ expectFailure.whenTesting().that(a).isEqualTo(b);
+ assertFailureKeys("expected", "but was");
+ assertFailureValue("expected", "Object 2");
+ assertFailureValue("but was", "Object 1");
+ }
+
+ @Test
+ public void isEqualToFailureWithDifferentTypesAndSameToString() {
+ Object a = "true";
+ Object b = true;
+ expectFailure.whenTesting().that(a).isEqualTo(b);
+ assertFailureKeys("expected", "an instance of", "but was", "an instance of");
+ assertFailureValue("expected", "true");
+ assertFailureValueIndexed("an instance of", 0, "java.lang.Boolean");
+ assertFailureValue("but was", "(non-equal value with same string representation)");
+ assertFailureValueIndexed("an instance of", 1, "java.lang.String");
+ }
+
+ @Test
+ public void isEqualToNullBadEqualsImplementation() {
+ expectFailure.whenTesting().that(new ThrowsOnEqualsNull()).isEqualTo(null);
+ }
+
+ @SuppressWarnings("TruthSelfEquals")
+ @Test
+ public void isEqualToSameInstanceBadEqualsImplementation() {
+ Object o = new ThrowsOnEquals();
+ assertThat(o).isEqualTo(o);
+ }
+
+ @Test
+ public void isNotEqualToWithNulls() {
+ Object o = null;
+ assertThat(o).isNotEqualTo("a");
+ }
+
+ @Test
+ public void isNotEqualToFailureWithNulls() {
+ Object o = null;
+ expectFailure.whenTesting().that(o).isNotEqualTo(null);
+ assertFailureKeys("expected not to be");
+ assertFailureValue("expected not to be", "null");
+ }
+
+ @Test
+ public void isNotEqualToWithObjects() {
+ Object a = new Object();
+ Object b = new Object();
+ assertThat(a).isNotEqualTo(b);
+ }
+
+ @Test
+ public void isNotEqualToFailureWithObjects() {
+ Object o = new Integer(1);
+ expectFailure.whenTesting().that(o).isNotEqualTo(new Integer(1));
+ assertFailureKeys("expected not to be");
+ assertFailureValue("expected not to be", "1");
+ }
+
+ @Test
+ public void isNotEqualToFailureWithSameObject() {
+ Object a = OBJECT_1;
+ Object b = a;
+ expectFailure.whenTesting().that(a).isNotEqualTo(b);
+ }
+
+ @Test
+ public void isNotEqualToWithDifferentTypesAndSameToString() {
+ Object a = "true";
+ Object b = true;
+ assertThat(a).isNotEqualTo(b);
+ }
+
+ @Test
+ public void isNotEqualToNullBadEqualsImplementation() {
+ assertThat(new ThrowsOnEqualsNull()).isNotEqualTo(null);
+ }
+
+ @SuppressWarnings("TruthSelfEquals")
+ @Test
+ public void isNotEqualToSameInstanceBadEqualsImplementation() {
+ Object o = new ThrowsOnEquals();
+ expectFailure.whenTesting().that(o).isNotEqualTo(o);
+ }
+
+ @Test
+ public void isInstanceOfExactType() {
+ assertThat("a").isInstanceOf(String.class);
+ }
+
+ @Test
+ public void isInstanceOfSuperclass() {
+ assertThat(3).isInstanceOf(Number.class);
+ }
+
+ @Test
+ public void isInstanceOfImplementedInterface() {
+ if (isGwt()) {
+ try {
+ assertThat("a").isInstanceOf(CharSequence.class);
+ fail();
+ } catch (UnsupportedOperationException expected) {
+ }
+ return;
+ }
+
+ assertThat("a").isInstanceOf(CharSequence.class);
+ }
+
+ @Test
+ public void isInstanceOfUnrelatedClass() {
+ expectFailure.whenTesting().that(4.5).isInstanceOf(Long.class);
+ assertFailureKeys("expected instance of", "but was instance of", "with value");
+ assertFailureValue("expected instance of", "java.lang.Long");
+ assertFailureValue("but was instance of", "java.lang.Double");
+ assertFailureValue("with value", "4.5");
+ }
+
+ @Test
+ public void isInstanceOfUnrelatedInterface() {
+ if (isGwt()) {
+ try {
+ assertThat(4.5).isInstanceOf(CharSequence.class);
+ fail();
+ } catch (UnsupportedOperationException expected) {
+ }
+ return;
+ }
+
+ expectFailure.whenTesting().that(4.5).isInstanceOf(CharSequence.class);
+ }
+
+ @Test
+ public void isInstanceOfClassForNull() {
+ expectFailure.whenTesting().that((Object) null).isInstanceOf(Long.class);
+ assertFailureKeys("expected instance of", "but was");
+ assertFailureValue("expected instance of", "java.lang.Long");
+ }
+
+ @Test
+ public void isInstanceOfInterfaceForNull() {
+ expectFailure.whenTesting().that((Object) null).isInstanceOf(CharSequence.class);
+ }
+
+ @Test
+ public void isNotInstanceOfUnrelatedClass() {
+ assertThat("a").isNotInstanceOf(Long.class);
+ }
+
+ @Test
+ public void isNotInstanceOfUnrelatedInterface() {
+ if (isGwt()) {
+ try {
+ assertThat(5).isNotInstanceOf(CharSequence.class);
+ fail();
+ } catch (UnsupportedOperationException expected) {
+ }
+ return;
+ }
+
+ assertThat(5).isNotInstanceOf(CharSequence.class);
+ }
+
+ @Test
+ public void isNotInstanceOfExactType() {
+ expectFailure.whenTesting().that(5).isNotInstanceOf(Integer.class);
+ assertFailureKeys("expected not to be an instance of", "but was");
+ assertFailureValue("expected not to be an instance of", "java.lang.Integer");
+ }
+
+ @Test
+ public void isNotInstanceOfSuperclass() {
+ expectFailure.whenTesting().that(5).isNotInstanceOf(Number.class);
+ }
+
+ @Test
+ public void isNotInstanceOfImplementedInterface() {
+ if (isGwt()) {
+ try {
+ assertThat("a").isNotInstanceOf(CharSequence.class);
+ fail();
+ } catch (UnsupportedOperationException expected) {
+ }
+ return;
+ }
+
+ expectFailure.whenTesting().that("a").isNotInstanceOf(CharSequence.class);
+ }
+
+ @Test
+ public void isIn() {
+ assertThat("b").isIn(oneShotIterable("a", "b", "c"));
+ }
+
+ @Test
+ public void isInJustTwo() {
+ assertThat("b").isIn(oneShotIterable("a", "b"));
+ }
+
+ @Test
+ public void isInFailure() {
+ expectFailure.whenTesting().that("x").isIn(oneShotIterable("a", "b", "c"));
+ assertFailureKeys("expected any of", "but was");
+ assertFailureValue("expected any of", "[a, b, c]");
+ }
+
+ @Test
+ public void isInNullInListWithNull() {
+ assertThat((String) null).isIn(oneShotIterable("a", "b", (String) null));
+ }
+
+ @Test
+ public void isInNonnullInListWithNull() {
+ assertThat("b").isIn(oneShotIterable("a", "b", (String) null));
+ }
+
+ @Test
+ public void isInNullFailure() {
+ expectFailure.whenTesting().that((String) null).isIn(oneShotIterable("a", "b", "c"));
+ }
+
+ @Test
+ public void isInEmptyFailure() {
+ expectFailure.whenTesting().that("b").isIn(ImmutableList.<String>of());
+ }
+
+ @Test
+ public void isAnyOf() {
+ assertThat("b").isAnyOf("a", "b", "c");
+ }
+
+ @Test
+ public void isAnyOfJustTwo() {
+ assertThat("b").isAnyOf("a", "b");
+ }
+
+ @Test
+ public void isAnyOfFailure() {
+ expectFailure.whenTesting().that("x").isAnyOf("a", "b", "c");
+ assertFailureKeys("expected any of", "but was");
+ assertFailureValue("expected any of", "[a, b, c]");
+ }
+
+ @Test
+ public void isAnyOfNullInListWithNull() {
+ assertThat((String) null).isAnyOf("a", "b", (String) null);
+ }
+
+ @Test
+ public void isAnyOfNonnullInListWithNull() {
+ assertThat("b").isAnyOf("a", "b", (String) null);
+ }
+
+ @Test
+ public void isAnyOfNullFailure() {
+ expectFailure.whenTesting().that((String) null).isAnyOf("a", "b", "c");
+ }
+
+ @Test
+ public void isNotIn() {
+ assertThat("x").isNotIn(oneShotIterable("a", "b", "c"));
+ }
+
+ @Test
+ public void isNotInFailure() {
+ expectFailure.whenTesting().that("b").isNotIn(oneShotIterable("a", "b", "c"));
+ assertFailureKeys("expected not to be any of", "but was");
+ assertFailureValue("expected not to be any of", "[a, b, c]");
+ }
+
+ @Test
+ public void isNotInNull() {
+ assertThat((String) null).isNotIn(oneShotIterable("a", "b", "c"));
+ }
+
+ @Test
+ public void isNotInNullFailure() {
+ expectFailure
+ .whenTesting()
+ .that((String) null)
+ .isNotIn(oneShotIterable("a", "b", (String) null));
+ }
+
+ @Test
+ public void isNotInEmpty() {
+ assertThat("b").isNotIn(ImmutableList.<String>of());
+ }
+
+ @Test
+ public void isNoneOf() {
+ assertThat("x").isNoneOf("a", "b", "c");
+ }
+
+ @Test
+ public void isNoneOfFailure() {
+ expectFailure.whenTesting().that("b").isNoneOf("a", "b", "c");
+ assertFailureKeys("expected not to be any of", "but was");
+ assertFailureValue("expected not to be any of", "[a, b, c]");
+ }
+
+ @Test
+ public void isNoneOfNull() {
+ assertThat((String) null).isNoneOf("a", "b", "c");
+ }
+
+ @Test
+ public void isNoneOfNullFailure() {
+ expectFailure.whenTesting().that((String) null).isNoneOf("a", "b", (String) null);
+ }
+
+ @Test
+ @SuppressWarnings({"EqualsIncompatibleType", "DoNotCall"})
+ public void equalsThrowsUSOE() {
+ try {
+ boolean unused = assertThat(5).equals(5);
+ } catch (UnsupportedOperationException expected) {
+ assertThat(expected)
+ .hasMessageThat()
+ .isEqualTo(
+ "Subject.equals() is not supported. Did you mean to call"
+ + " assertThat(actual).isEqualTo(expected) instead of"
+ + " assertThat(actual).equals(expected)?");
+ return;
+ }
+ fail("Should have thrown.");
+ }
+
+ @Test
+ @SuppressWarnings("DoNotCall")
+ public void hashCodeThrowsUSOE() {
+ try {
+ int unused = assertThat(5).hashCode();
+ } catch (UnsupportedOperationException expected) {
+ assertThat(expected).hasMessageThat().isEqualTo("Subject.hashCode() is not supported.");
+ return;
+ }
+ fail("Should have thrown.");
+ }
+
+ @Test
+ public void ignoreCheckDiscardsFailures() {
+ assertThat((Object) null).ignoreCheck().that("foo").isNull();
+ }
+
+ private static <T> Iterable<T> oneShotIterable(final T... values) {
+ final Iterator<T> iterator = Iterators.forArray(values);
+ return new Iterable<T>() {
+ @Override
+ public Iterator<T> iterator() {
+ return iterator;
+ }
+
+ @Override
+ public String toString() {
+ return Arrays.toString(values);
+ }
+ };
+ }
+
+ @Test
+ public void disambiguationWithSameToString() {
+ expectFailure.whenTesting().that(new StringBuilder("foo")).isEqualTo(new StringBuilder("foo"));
+ assertFailureKeys("expected", "but was");
+ assertFailureValue("expected", "foo");
+ assertFailureValue(
+ "but was", "(non-equal instance of same class with same string representation)");
+ }
+
+ private static final class ThrowsOnEqualsNull {
+
+ @SuppressWarnings("EqualsHashCode")
+ @Override
+ public boolean equals(Object obj) {
+ checkNotNull(obj); // buggy implementation but one that we're working around, at least for now
+ return super.equals(obj);
+ }
+ }
+
+ private static final class ThrowsOnEquals {
+
+ @SuppressWarnings("EqualsHashCode")
+ @Override
+ public boolean equals(Object obj) {
+ throw new UnsupportedOperationException();
+ // buggy implementation but one that we're working around, at least for now
+ }
+ }
+
+ private static final class ForbidsEqualityChecksSubject extends Subject {
+ ForbidsEqualityChecksSubject(FailureMetadata metadata, @Nullable Object actual) {
+ super(metadata, actual);
+ }
+
+ // Not sure how to feel about this, but people do it:
+
+ @Override
+ public void isEqualTo(@Nullable Object expected) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void isNotEqualTo(@Nullable Object unexpected) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ private static Subject.Factory<ForbidsEqualityChecksSubject, Object>
+ objectsForbiddingEqualityCheck() {
+ return new Factory<ForbidsEqualityChecksSubject, Object>() {
+ @Override
+ public ForbidsEqualityChecksSubject createSubject(FailureMetadata metadata, Object actual) {
+ return new ForbidsEqualityChecksSubject(metadata, actual);
+ }
+ };
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/TableSubjectTest.java b/core/src/test/java/com/google/common/truth/TableSubjectTest.java
new file mode 100644
index 00000000..aea450c2
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/TableSubjectTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableTable;
+import com.google.common.collect.Table;
+import com.google.common.collect.Table.Cell;
+import com.google.common.collect.Tables;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Table Subjects.
+ *
+ * @author Kurt Alfred Kluever
+ */
+@RunWith(JUnit4.class)
+public class TableSubjectTest extends BaseSubjectTestCase {
+
+ @Test
+ public void tableIsEmpty() {
+ ImmutableTable<String, String, String> table = ImmutableTable.of();
+ assertThat(table).isEmpty();
+ }
+
+ @Test
+ public void tableIsEmptyWithFailure() {
+ ImmutableTable<Integer, Integer, Integer> table = ImmutableTable.of(1, 5, 7);
+ expectFailureWhenTestingThat(table).isEmpty();
+ assertFailureKeys("expected to be empty", "but was");
+ }
+
+ @Test
+ public void tableIsNotEmpty() {
+ ImmutableTable<Integer, Integer, Integer> table = ImmutableTable.of(1, 5, 7);
+ assertThat(table).isNotEmpty();
+ }
+
+ @Test
+ public void tableIsNotEmptyWithFailure() {
+ ImmutableTable<Integer, Integer, Integer> table = ImmutableTable.of();
+ expectFailureWhenTestingThat(table).isNotEmpty();
+ assertFailureKeys("expected not to be empty");
+ }
+
+ @Test
+ public void hasSize() {
+ assertThat(ImmutableTable.of(1, 2, 3)).hasSize(1);
+ }
+
+ @Test
+ public void hasSizeZero() {
+ assertThat(ImmutableTable.of()).hasSize(0);
+ }
+
+ @Test
+ public void hasSizeNegative() {
+ try {
+ assertThat(ImmutableTable.of(1, 2, 3)).hasSize(-1);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test
+ public void contains() {
+ ImmutableTable<String, String, String> table = ImmutableTable.of("row", "col", "val");
+ assertThat(table).contains("row", "col");
+ }
+
+ @Test
+ public void containsFailure() {
+ ImmutableTable<String, String, String> table = ImmutableTable.of("row", "col", "val");
+ expectFailureWhenTestingThat(table).contains("row", "row");
+ assertThat(expectFailure.getFailure())
+ .hasMessageThat()
+ .isEqualTo("Not true that <{row={col=val}}> contains mapping for row/column <row> <row>");
+ }
+
+ @Test
+ public void doesNotContain() {
+ ImmutableTable<String, String, String> table = ImmutableTable.of("row", "col", "val");
+ assertThat(table).doesNotContain("row", "row");
+ assertThat(table).doesNotContain("col", "row");
+ assertThat(table).doesNotContain("col", "col");
+ assertThat(table).doesNotContain(null, null);
+ }
+
+ @Test
+ public void doesNotContainFailure() {
+ ImmutableTable<String, String, String> table = ImmutableTable.of("row", "col", "val");
+ expectFailureWhenTestingThat(table).doesNotContain("row", "col");
+ assertThat(expectFailure.getFailure())
+ .hasMessageThat()
+ .isEqualTo(
+ "Not true that <{row={col=val}}> does not contain mapping for "
+ + "row/column <row> <col>");
+ }
+
+ @Test
+ public void containsCell() {
+ ImmutableTable<String, String, String> table = ImmutableTable.of("row", "col", "val");
+ assertThat(table).containsCell("row", "col", "val");
+ assertThat(table).containsCell(cell("row", "col", "val"));
+ }
+
+ @Test
+ public void containsCellFailure() {
+ ImmutableTable<String, String, String> table = ImmutableTable.of("row", "col", "val");
+ expectFailureWhenTestingThat(table).containsCell("row", "row", "val");
+ assertFailureKeys("value of", "expected to contain", "but was");
+ assertFailureValue("value of", "table.cellSet()");
+ assertFailureValue("expected to contain", "(row,row)=val");
+ assertFailureValue("but was", "[(row,col)=val]");
+ }
+
+ @Test
+ public void doesNotContainCell() {
+ ImmutableTable<String, String, String> table = ImmutableTable.of("row", "col", "val");
+ assertThat(table).doesNotContainCell("row", "row", "val");
+ assertThat(table).doesNotContainCell("col", "row", "val");
+ assertThat(table).doesNotContainCell("col", "col", "val");
+ assertThat(table).doesNotContainCell(null, null, null);
+ assertThat(table).doesNotContainCell(cell("row", "row", "val"));
+ assertThat(table).doesNotContainCell(cell("col", "row", "val"));
+ assertThat(table).doesNotContainCell(cell("col", "col", "val"));
+ assertThat(table).doesNotContainCell(cell(null, null, null));
+ }
+
+ @Test
+ public void doesNotContainCellFailure() {
+ ImmutableTable<String, String, String> table = ImmutableTable.of("row", "col", "val");
+ expectFailureWhenTestingThat(table).doesNotContainCell("row", "col", "val");
+ assertFailureKeys("value of", "expected not to contain", "but was");
+ assertFailureValue("value of", "table.cellSet()");
+ assertFailureValue("expected not to contain", "(row,col)=val");
+ assertFailureValue("but was", "[(row,col)=val]");
+ }
+
+ private static <R, C, V> Cell<R, C, V> cell(R row, C col, V val) {
+ return Tables.immutableCell(row, col, val);
+ }
+
+ private TableSubject expectFailureWhenTestingThat(Table actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/TestCorrespondences.java b/core/src/test/java/com/google/common/truth/TestCorrespondences.java
new file mode 100644
index 00000000..d9cca3e1
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/TestCorrespondences.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Objects;
+import com.google.common.base.Splitter;
+import com.google.common.primitives.Ints;
+import java.util.List;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** {@link Correspondence} implementations for testing purposes. */
+final class TestCorrespondences {
+ /**
+ * A correspondence between strings and integers which tests whether the string parses as the
+ * integer. Parsing is as specified by {@link Integer#decode(String)}. It considers null to
+ * correspond to null only.
+ */
+ static final Correspondence<String, Integer> STRING_PARSES_TO_INTEGER_CORRESPONDENCE =
+ Correspondence.from(
+ // If we were allowed to use method references, this would be:
+ // TestCorrespondences::stringParsesToInteger,
+ new Correspondence.BinaryPredicate<String, Integer>() {
+ @Override
+ public boolean apply(@Nullable String actual, @Nullable Integer expected) {
+ return stringParsesToInteger(actual, expected);
+ }
+ },
+ "parses to");
+
+ private static boolean stringParsesToInteger(
+ @Nullable String actual, @Nullable Integer expected) {
+ if (actual == null) {
+ return expected == null;
+ }
+ try {
+ // Older versions of Android reject leading plus signs, per the pre-Java-7 contract:
+ // https://docs.oracle.com/javase/6/docs/api/java/lang/Integer.html#decode(java.lang.String)
+ // https://docs.oracle.com/javase/7/docs/api/java/lang/Integer.html#decode(java.lang.String)
+ if (actual.startsWith("+")) {
+ actual = actual.substring(1);
+ }
+ return Integer.decode(actual).equals(expected);
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
+ /** A formatter for the diffs between integers. */
+ static final Correspondence.DiffFormatter<Integer, Integer> INT_DIFF_FORMATTER =
+ // If we were allowed to use lambdas, this would be:
+ // (a, e) -> Integer.toString(a - e));
+ new Correspondence.DiffFormatter<Integer, Integer>() {
+ @Override
+ public String formatDiff(Integer actual, Integer expected) {
+ return Integer.toString(actual - expected);
+ }
+ };
+
+ /**
+ * A correspondence between integers which tests whether they are within 10 of each other. Smart
+ * diffing is enabled, with a formatted diff showing the actual value less the expected value.
+ * Does not support null values.
+ */
+ static final Correspondence<Integer, Integer> WITHIN_10_OF =
+ Correspondence.from(
+ // If we were allowed to use lambdas, this would be:
+ // (Integer a, Integer e) -> Math.abs(a - e) <= 10,
+ new Correspondence.BinaryPredicate<Integer, Integer>() {
+ @Override
+ public boolean apply(Integer actual, Integer expected) {
+ return Math.abs(actual - expected) <= 10;
+ }
+ },
+ "is within 10 of")
+ .formattingDiffsUsing(INT_DIFF_FORMATTER);
+
+ /**
+ * A correspondence between strings which tests for case-insensitive equality. Supports null
+ * expected elements, but throws {@link NullPointerException} on null actual elements.
+ */
+ static final Correspondence<String, String> CASE_INSENSITIVE_EQUALITY =
+ Correspondence.from(
+ // If we were allowed to use method references, this would be String::equalsIgnoreCase.
+ new Correspondence.BinaryPredicate<String, String>() {
+ @Override
+ public boolean apply(String actual, String expected) {
+ return actual.equalsIgnoreCase(expected);
+ }
+ },
+ "equals (ignoring case)");
+
+ /**
+ * A correspondence between strings which tests for case-insensitive equality, with a broken
+ * attempt at null-safety. The {@link Correspondence#compare} implementation returns true for
+ * (null, null) and false for (non-null, null), but throws {@link NullPointerException} for (null,
+ * non-null).
+ */
+ static final Correspondence<String, String> CASE_INSENSITIVE_EQUALITY_HALF_NULL_SAFE =
+ Correspondence.from(
+ // If we were allowed to use method references, this would be:
+ // TestCorrespondences::equalsIgnoreCaseHalfNullSafe,
+ new Correspondence.BinaryPredicate<String, String>() {
+ @Override
+ public boolean apply(String actual, String expected) {
+ return equalsIgnoreCaseHalfNullSafe(actual, expected);
+ }
+ },
+ "equals (ignoring case)");
+
+ private static boolean equalsIgnoreCaseHalfNullSafe(String actual, String expected) {
+ if (actual == null && expected == null) {
+ return true;
+ }
+ // Oops! We don't handle the case where actual == null but expected != null.
+ return actual.equalsIgnoreCase(expected);
+ }
+
+ /**
+ * An example value object. It has an optional {@code id} field and a required {@code score}
+ * field, both positive integers.
+ */
+ static final class Record {
+ private final int id;
+ private final int score;
+
+ static Record create(int id, int score) {
+ checkState(id >= 0);
+ checkState(score > 0);
+ return new Record(id, score);
+ }
+
+ static Record createWithoutId(int score) {
+ checkState(score >= 0);
+ return new Record(-1, score);
+ }
+
+ Record(int id, int score) {
+ this.id = id;
+ this.score = score;
+ }
+
+ boolean hasId() {
+ return id >= 0;
+ }
+
+ int getId() {
+ checkState(hasId());
+ return id;
+ }
+
+ int getScore() {
+ return score;
+ }
+
+ boolean hasSameId(Record that) {
+ return this.id == that.id;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o instanceof Record) {
+ Record that = (Record) o;
+ return this.id == that.id && this.score == that.score;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(id, score);
+ }
+
+ /**
+ * Returns the string form of the record, which is the {@code id} value or the literal {@code
+ * none} if none, the literal {@code /}, and the {@code score} value concatenated.
+ */
+ @Override
+ public String toString() {
+ return Joiner.on('/').join(hasId() ? getId() : "none", getScore());
+ }
+
+ /**
+ * If the argument is the string form of a record, returns that record; otherwise returns {@code
+ * null}.
+ */
+ static @Nullable Record parse(String str) {
+ List<String> parts = Splitter.on('/').splitToList(str);
+ if (parts.size() != 2) {
+ return null;
+ }
+ @Nullable Integer id = parts.get(0).equals("none") ? -1 : Ints.tryParse(parts.get(0));
+ @Nullable Integer score = Ints.tryParse(parts.get(1));
+ if (id == null || score == null) {
+ return null;
+ }
+ return new Record(id, score);
+ }
+ }
+
+ /**
+ * A correspondence between {@link Record} instances which tests whether their {@code id} values
+ * are equal and their {@code score} values are within 10 of each other. Smart diffing is not
+ * supported.
+ *
+ * <p>The {@link Correspondence#compare} implementation support nulls, such that null corresponds
+ * to null only. The {@link Correspondence#formatDiff} implementation does not support nulls.
+ */
+ static final Correspondence<Record, Record> RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10_NO_DIFF =
+ Correspondence.from(
+ // If we were allowed to use method references, this would be:
+ // TestCorrespondences::recordsAreCloseEnough,
+ new Correspondence.BinaryPredicate<Record, Record>() {
+ @Override
+ public boolean apply(Record actual, Record expected) {
+ return recordsAreCloseEnough(actual, expected);
+ }
+ },
+ "has the same id as and a score within 10 of");
+
+ /**
+ * A formatter for diffs between records. If the records have the same key, it gives a string of
+ * the form {@code "score:<score_diff>"}. If they have different keys, it gives null.
+ */
+ static final Correspondence.DiffFormatter<Record, Record> RECORD_DIFF_FORMATTER =
+ // If we were allowed to use method references, this would be:
+ // TestCorrespondences::formatRecordDiff);
+ new Correspondence.DiffFormatter<Record, Record>() {
+ @Override
+ public String formatDiff(Record actual, Record expected) {
+ return formatRecordDiff(actual, expected);
+ }
+ };
+
+ /**
+ * A correspondence between {@link Record} instances which tests whether their {@code id} values
+ * are equal and their {@code score} values are within 10 of each other. Smart diffing is enabled
+ * for records with equal {@code id} values, with a formatted diff showing the actual {@code
+ * score} value less the expected {@code score} value preceded by the literal {@code score:}.
+ *
+ * <p>The {@link Correspondence#compare} implementation support nulls, such that null corresponds
+ * to null only. The {@link Correspondence#formatDiff} implementation does not support nulls.
+ */
+ static final Correspondence<Record, Record> RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10 =
+ RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10_NO_DIFF.formattingDiffsUsing(RECORD_DIFF_FORMATTER);
+
+ /**
+ * A correspondence like {@link #RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10} except that the actual
+ * values are strings which will be parsed before comparing. If the string does not parse to a
+ * record then it does not correspond and is not diffed. Does not support null strings or records.
+ */
+ static final Correspondence<String, Record> PARSED_RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10 =
+ Correspondence.from(
+ // If we were allowed to use lambdas, this would be:
+ // (String a, Record e) -> {
+ // @Nullable Record actualRecord = Record.parse(a);
+ // return actualRecord != null && recordsAreCloseEnough(actualRecord, e);
+ // },
+ new Correspondence.BinaryPredicate<String, Record>() {
+ @Override
+ public boolean apply(String actual, Record expected) {
+ @Nullable Record actualRecord = Record.parse(actual);
+ return actualRecord != null && recordsAreCloseEnough(actualRecord, expected);
+ }
+ },
+ "parses to a record that " + RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
+ .formattingDiffsUsing(
+ // If we were allowe to use lambdas, this would be:
+ // (a, e) -> {
+ // @Nullable Record actualRecord = Record.parse(a);
+ // return actualRecord != null ? formatRecordDiff(actualRecord, e) : null;
+ // });
+ new Correspondence.DiffFormatter<String, Record>() {
+ @Override
+ public String formatDiff(String actual, Record expected) {
+ @Nullable Record actualRecord = Record.parse(actual);
+ return actualRecord != null ? formatRecordDiff(actualRecord, expected) : null;
+ }
+ });
+
+ private static boolean recordsAreCloseEnough(@Nullable Record actual, @Nullable Record expected) {
+ if (actual == null) {
+ return expected == null;
+ }
+ if (expected == null) {
+ return false;
+ }
+ return actual.hasSameId(expected) && Math.abs(actual.getScore() - expected.getScore()) <= 10;
+ }
+
+ private static String formatRecordDiff(Record actual, Record expected) {
+ if (actual.hasId() && expected.hasId() && actual.getId() == expected.getId()) {
+ return "score:" + (actual.getScore() - expected.getScore());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * A key function for {@link Record} instances that keys records by their {@code id} values. The
+ * key is null if the record has no {@code id}. Does not support null records.
+ */
+ static final Function<Record, Integer> RECORD_ID =
+ new Function<Record, Integer>() {
+
+ @Override
+ public @Nullable Integer apply(Record record) {
+ return record.hasId() ? record.getId() : null;
+ }
+ };
+
+ /**
+ * A key function for {@link Record} instances that keys records by their {@code id} values. The
+ * key is null if the record has no {@code id}. Does not support null records.
+ */
+ static final Function<Record, Integer> NULL_SAFE_RECORD_ID =
+ new Function<Record, Integer>() {
+
+ @Override
+ public @Nullable Integer apply(Record record) {
+ if (record == null) {
+ return 0;
+ }
+ return record.hasId() ? record.getId() : null;
+ }
+ };
+
+ /**
+ * A key function for {@link String} instances that attempts to parse them as {@link Record}
+ * instances and keys records by their {@code id} values. The key is null if the string does not
+ * parse or the record has no {@code id}. Does not support null strings.
+ */
+ static final Function<String, Integer> PARSED_RECORD_ID =
+ new Function<String, Integer>() {
+
+ @Override
+ public @Nullable Integer apply(String str) {
+ Record record = Record.parse(str);
+ return record != null ? RECORD_ID.apply(record) : null;
+ }
+ };
+
+ private TestCorrespondences() {}
+}
diff --git a/core/src/test/java/com/google/common/truth/TestPlatform.java b/core/src/test/java/com/google/common/truth/TestPlatform.java
new file mode 100644
index 00000000..852a8440
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/TestPlatform.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2018 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+final class TestPlatform {
+ static boolean isGwt() {
+ return false;
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/ThrowableSubjectTest.java b/core/src/test/java/com/google/common/truth/ThrowableSubjectTest.java
new file mode 100644
index 00000000..bbf33e4e
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/ThrowableSubjectTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link Throwable} subjects.
+ *
+ * @author Kurt Alfred Kluever
+ */
+@RunWith(JUnit4.class)
+public class ThrowableSubjectTest extends BaseSubjectTestCase {
+
+ @Test
+ public void hasMessageThat() {
+ NullPointerException npe = new NullPointerException("message");
+ assertThat(npe).hasMessageThat().isEqualTo("message");
+ }
+
+ @Test
+ public void hasMessageThat_null() {
+ assertThat(new NullPointerException()).hasMessageThat().isNull();
+ assertThat(new NullPointerException(null)).hasMessageThat().isNull();
+ }
+
+ @Test
+ public void hasMessageThat_failure() {
+ NullPointerException actual = new NullPointerException("message");
+ expectFailureWhenTestingThat(actual).hasMessageThat().isEqualTo("foobar");
+ assertFailureValue("value of", "throwable.getMessage()");
+ assertErrorHasActualAsCause(actual, expectFailure.getFailure());
+ }
+
+ @Test
+ public void hasMessageThat_MessageHasNullMessage_failure() {
+ expectFailureWhenTestingThat(new NullPointerException("message")).hasMessageThat().isNull();
+ }
+
+ @Test
+ public void hasMessageThat_NullMessageHasMessage_failure() {
+ NullPointerException npe = new NullPointerException(null);
+ expectFailureWhenTestingThat(npe).hasMessageThat().isEqualTo("message");
+ }
+
+ @Test
+ public void hasCauseThat_message() {
+ assertThat(new Exception("foobar", new IOException("barfoo")))
+ .hasCauseThat()
+ .hasMessageThat()
+ .isEqualTo("barfoo");
+ }
+
+ @Test
+ public void hasCauseThat_instanceOf() {
+ assertThat(new Exception("foobar", new IOException("barfoo")))
+ .hasCauseThat()
+ .isInstanceOf(IOException.class);
+ }
+
+ @Test
+ public void hasCauseThat_null() {
+ assertThat(new Exception("foobar")).hasCauseThat().isNull();
+ }
+
+ @Test
+ public void hasCauseThat_message_failure() {
+ Exception actual = new Exception("foobar", new IOException("barfoo"));
+ expectFailureWhenTestingThat(actual).hasCauseThat().hasMessageThat().isEqualTo("message");
+ assertFailureValue("value of", "throwable.getCause().getMessage()");
+ assertErrorHasActualAsCause(actual, expectFailure.getFailure());
+ }
+
+ @Test
+ public void hasCauseThat_instanceOf_failure() {
+ Exception actual = new Exception("foobar", new IOException("barfoo"));
+ expectFailureWhenTestingThat(actual).hasCauseThat().isInstanceOf(RuntimeException.class);
+ assertFailureValue("value of", "throwable.getCause()");
+ assertErrorHasActualAsCause(actual, expectFailure.getFailure());
+ }
+
+ @Test
+ public void hasCauseThat_tooDeep_failure() {
+ Exception actual = new Exception("foobar");
+ expectFailureWhenTestingThat(actual).hasCauseThat().hasCauseThat().isNull();
+ assertThat(expectFailure.getFailure().getMessage())
+ .isEqualTo(
+ "Causal chain is not deep enough - add a .isNotNull() check?\n"
+ + "value of: throwable.getCause().getCause()");
+ assertErrorHasActualAsCause(actual, expectFailure.getFailure());
+ }
+
+ @Test
+ public void hasCauseThat_deepNull_failure() {
+ Exception actual =
+ new Exception("foobar", new RuntimeException("barfoo", new IOException("buzz")));
+ expectFailureWhenTestingThat(actual)
+ .hasCauseThat()
+ .hasCauseThat()
+ .hasMessageThat()
+ .isEqualTo("message");
+ assertFailureValue("value of", "throwable.getCause().getCause().getMessage()");
+ assertErrorHasActualAsCause(actual, expectFailure.getFailure());
+ }
+
+ @Test
+ public void inheritedMethodChainsSubject() {
+ NullPointerException expected = new NullPointerException("expected");
+ NullPointerException actual = new NullPointerException("actual");
+ expectFailureWhenTestingThat(actual).isEqualTo(expected);
+ assertErrorHasActualAsCause(actual, expectFailure.getFailure());
+ }
+
+ private static void assertErrorHasActualAsCause(Throwable actual, AssertionError failure) {
+ assertWithMessage("AssertionError's cause").that(failure.getCause()).isEqualTo(actual);
+ }
+
+ private ThrowableSubject expectFailureWhenTestingThat(Throwable actual) {
+ return expectFailure.whenTesting().that(actual);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/TruthAssertThatTest.java b/core/src/test/java/com/google/common/truth/TruthAssertThatTest.java
new file mode 100644
index 00000000..90769484
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/TruthAssertThatTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assert_;
+import static java.util.Arrays.asList;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Ordering;
+import com.google.common.reflect.TypeToken;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for the FEST-alike assertThat() entry point.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+@RunWith(JUnit4.class)
+public class TruthAssertThatTest {
+ private static final Function<Method, TypeToken<?>> METHOD_TO_RETURN_TYPE_TOKEN =
+ new Function<Method, TypeToken<?>>() {
+ @Override
+ public TypeToken<?> apply(Method input) {
+ return TypeToken.of(Iterables.getOnlyElement(asList(input.getParameterTypes())));
+ }
+ };
+
+ @Test
+ public void staticAssertThatMethodsMatchStandardSubjectBuilderInstanceMethods() {
+ ImmutableSet<TypeToken<?>> verbTypes =
+ FluentIterable.from(asList(StandardSubjectBuilder.class.getMethods()))
+ .filter(
+ new Predicate<Method>() {
+ @Override
+ public boolean apply(Method input) {
+ return input.getName().equals("that");
+ }
+ })
+ .transform(METHOD_TO_RETURN_TYPE_TOKEN)
+ .toSortedSet(Ordering.usingToString());
+ ImmutableSet<TypeToken<?>> truthTypes =
+ FluentIterable.from(asList(Truth.class.getMethods()))
+ .filter(
+ new Predicate<Method>() {
+ @Override
+ public boolean apply(Method input) {
+ return input.getName().equals("assertThat")
+ && Modifier.isStatic(input.getModifiers());
+ }
+ })
+ .transform(METHOD_TO_RETURN_TYPE_TOKEN)
+ .toSortedSet(Ordering.usingToString());
+
+ assert_().that(verbTypes).isNotEmpty();
+ assert_().that(truthTypes).isNotEmpty();
+ assert_().that(truthTypes).containsExactlyElementsIn(verbTypes);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/TruthFailureSubjectTest.java b/core/src/test/java/com/google/common/truth/TruthFailureSubjectTest.java
new file mode 100644
index 00000000..cb9d7976
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/TruthFailureSubjectTest.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (c) 2018 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.Fact.simpleFact;
+import static com.google.common.truth.TruthFailureSubject.HOW_TO_TEST_KEYS_WITHOUT_VALUES;
+import static com.google.common.truth.TruthFailureSubject.truthFailures;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link TruthFailureSubject}. */
+@RunWith(JUnit4.class)
+public class TruthFailureSubjectTest extends BaseSubjectTestCase {
+ // factKeys()
+
+ @Test
+ public void factKeys() {
+ assertThat(fact("foo", "the foo")).factKeys().containsExactly("foo");
+ }
+
+ @Test
+ public void factKeysNoValue() {
+ assertThat(simpleFact("foo")).factKeys().containsExactly("foo");
+ }
+
+ @Test
+ public void factKeysFail() {
+ expectFailureWhenTestingThat(fact("foo", "the foo")).factKeys().containsExactly("bar");
+ Truth.assertThat(expectFailure.getFailure())
+ .hasMessageThat()
+ .contains("value of: failure.factKeys()");
+ // TODO(cpovirk): Switch to using fact-based assertions once IterableSubject uses them.
+ }
+
+ // factValue(String)
+
+ @Test
+ public void factValue() {
+ assertThat(fact("foo", "the foo")).factValue("foo").isEqualTo("the foo");
+ }
+
+ @Test
+ public void factValueFailWrongValue() {
+ expectFailureWhenTestingThat(fact("foo", "the foo")).factValue("foo").isEqualTo("the bar");
+ assertFailureValue("value of", "failure.factValue(foo)");
+ }
+
+ @Test
+ public void factValueFailNoSuchKey() {
+ Object unused = expectFailureWhenTestingThat(fact("foo", "the foo")).factValue("bar");
+ assertFailureKeys("expected to contain fact", "but contained only");
+ assertFailureValue("expected to contain fact", "bar");
+ assertFailureValue("but contained only", "[foo]");
+ }
+
+ @Test
+ public void factValueFailMultipleKeys() {
+ Object unused =
+ expectFailureWhenTestingThat(fact("foo", "the foo"), fact("foo", "the other foo"))
+ .factValue("foo");
+ assertFailureKeys("expected to contain a single fact with key", "but contained multiple");
+ assertFailureValue("expected to contain a single fact with key", "foo");
+ assertFailureValue("but contained multiple", "[foo: the foo, foo: the other foo]");
+ }
+
+ @Test
+ public void factValueFailNoValue() {
+ Object unused = expectFailureWhenTestingThat(simpleFact("foo")).factValue("foo");
+ assertFailureKeys(
+ "expected to have a value",
+ "for key",
+ "but the key was present with no value",
+ HOW_TO_TEST_KEYS_WITHOUT_VALUES.key);
+ assertFailureValue("for key", "foo");
+ }
+
+ // factValue(String, int)
+
+ @Test
+ public void factValueInt() {
+ assertThat(fact("foo", "the foo")).factValue("foo", 0).isEqualTo("the foo");
+ }
+
+ @Test
+ public void factValueIntMultipleKeys() {
+ assertThat(fact("foo", "the foo"), fact("foo", "the other foo"))
+ .factValue("foo", 1)
+ .isEqualTo("the other foo");
+ }
+
+ @Test
+ public void factValueIntFailNegative() {
+ try {
+ assertThat(fact("foo", "the foo")).factValue("foo", -1);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test
+ public void factValueIntFailWrongValue() {
+ expectFailureWhenTestingThat(fact("foo", "the foo")).factValue("foo", 0).isEqualTo("the bar");
+ assertFailureValue("value of", "failure.factValue(foo, 0)");
+ }
+
+ @Test
+ public void factValueIntFailNoSuchKey() {
+ Object unused = expectFailureWhenTestingThat(fact("foo", "the foo")).factValue("bar", 0);
+ assertFailureKeys("expected to contain fact", "but contained only");
+ assertFailureValue("expected to contain fact", "bar");
+ assertFailureValue("but contained only", "[foo]");
+ }
+
+ @Test
+ public void factValueIntFailNotEnoughWithKey() {
+ Object unused = expectFailureWhenTestingThat(fact("foo", "the foo")).factValue("foo", 5);
+ assertFailureKeys("for key", "index too high", "fact count was");
+ assertFailureValue("for key", "foo");
+ assertFailureValue("index too high", "5");
+ assertFailureValue("fact count was", "1");
+ }
+
+ @Test
+ public void factValueIntFailNoValue() {
+ Object unused = expectFailureWhenTestingThat(simpleFact("foo")).factValue("foo", 0);
+ assertFailureKeys(
+ "expected to have a value",
+ "for key",
+ "and index",
+ "but the key was present with no value",
+ HOW_TO_TEST_KEYS_WITHOUT_VALUES.key);
+ assertFailureValue("for key", "foo");
+ assertFailureValue("and index", "0");
+ }
+
+ // other tests
+
+ @Test
+ public void nonTruthErrorFactKeys() {
+ Object unused = expectFailureWhenTestingThat(new AssertionError()).factKeys();
+ assertFailureKeys("expected a failure thrown by Truth's new failure API", "but was");
+ }
+
+ @Test
+ public void nonTruthErrorFactValue() {
+ Object unused = expectFailureWhenTestingThat(new AssertionError()).factValue("foo");
+ assertFailureKeys("expected a failure thrown by Truth's new failure API", "but was");
+ }
+
+ private TruthFailureSubject assertThat(Fact... facts) {
+ return ExpectFailure.assertThat(failure(facts));
+ }
+
+ private TruthFailureSubject expectFailureWhenTestingThat(Fact... facts) {
+ return expectFailureWhenTestingThat(failure(facts));
+ }
+
+ private TruthFailureSubject expectFailureWhenTestingThat(AssertionError failure) {
+ return (TruthFailureSubject) expectFailure.whenTesting().about(truthFailures()).that(failure);
+ }
+
+ private AssertionErrorWithFacts failure(Fact... facts) {
+ return new AssertionErrorWithFacts(
+ ImmutableList.<String>of(), ImmutableList.copyOf(facts), /* cause= */ null);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/extension/Employee.java b/core/src/test/java/com/google/common/truth/extension/Employee.java
new file mode 100644
index 00000000..8cc2cf44
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/extension/Employee.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2015 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extension;
+
+import com.google.auto.value.AutoValue;
+
+/** Represents an employee. */
+@AutoValue
+public abstract class Employee {
+ public static Employee create(
+ String username, long id, String name, Location location, boolean isCeo) {
+ return new AutoValue_Employee(username, id, name, location, isCeo);
+ }
+
+ abstract String username();
+
+ abstract long id();
+
+ abstract String name();
+
+ abstract Location location();
+
+ abstract boolean isCeo();
+
+ public enum Location {
+ MTV,
+ PIT,
+ CHI,
+ NYC
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/extension/EmployeeSubject.java b/core/src/test/java/com/google/common/truth/extension/EmployeeSubject.java
new file mode 100644
index 00000000..854afb06
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/extension/EmployeeSubject.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2015 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extension;
+
+import static com.google.common.truth.Fact.simpleFact;
+import static com.google.common.truth.Truth.assertAbout;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A <a href="https://github.com/google/truth">Truth</a> subject for {@link Employee}.
+ *
+ * @author Kurt Alfred Kluever (kak@google.com)
+ */
+public final class EmployeeSubject extends Subject {
+
+ // User-defined entry point
+ public static EmployeeSubject assertThat(@Nullable Employee employee) {
+ return assertAbout(EMPLOYEE_SUBJECT_FACTORY).that(employee);
+ }
+
+ // Static method for getting the subject factory (for use with assertAbout())
+ public static Subject.Factory<EmployeeSubject, Employee> employees() {
+ return EMPLOYEE_SUBJECT_FACTORY;
+ }
+
+ // Boiler-plate Subject.Factory for EmployeeSubject
+ private static final Subject.Factory<EmployeeSubject, Employee> EMPLOYEE_SUBJECT_FACTORY =
+ EmployeeSubject::new;
+
+ private final Employee actual;
+
+ private EmployeeSubject(FailureMetadata failureMetadata, @Nullable Employee subject) {
+ super(failureMetadata, subject);
+ this.actual = subject;
+ }
+
+ // User-defined test assertion SPI below this point
+
+ public void hasName(String name) {
+ check("name()").that(actual.name()).isEqualTo(name);
+ }
+
+ public void hasUsername(String username) {
+ check("username()").that(actual.username()).isEqualTo(username);
+ }
+
+ public void hasId(long id) {
+ check("id()").that(actual.id()).isEqualTo(id);
+ }
+
+ public void hasLocation(Employee.Location location) {
+ check("location()").that(actual.location()).isEqualTo(location);
+ }
+
+ public void isCeo() {
+ if (!actual.isCeo()) {
+ failWithActual(simpleFact("expected to be CEO"));
+ }
+ }
+
+ public void isNotCeo() {
+ if (actual.isCeo()) {
+ failWithActual(simpleFact("expected not to be CEO"));
+ }
+ }
+
+ // TODO(kak): Add methods that return other subjects. E.g.,
+ // public StringSubject username() {}
+ // public IterableSubject languages() {}
+}
diff --git a/core/src/test/java/com/google/common/truth/extension/EmployeeSubjectTest.java b/core/src/test/java/com/google/common/truth/extension/EmployeeSubjectTest.java
new file mode 100644
index 00000000..8b60963d
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/extension/EmployeeSubjectTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2015 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extension;
+
+import static com.google.common.truth.ExpectFailure.assertThat;
+import static com.google.common.truth.ExpectFailure.expectFailureAbout;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extension.EmployeeSubject.assertThat;
+import static com.google.common.truth.extension.EmployeeSubject.employees;
+
+import com.google.common.truth.ExpectFailure.SimpleSubjectBuilderCallback;
+import com.google.common.truth.extension.Employee.Location;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class EmployeeSubjectTest {
+
+ // Note: not real employee IDs :-)
+
+ private static final Employee KURT =
+ Employee.create("kak", 37802, "Kurt Alfred Kluever", Location.NYC, false);
+
+ @Test
+ public void id() {
+ assertThat(KURT).hasId(37802);
+ expectFailure(whenTesting -> whenTesting.that(KURT).hasId(12345));
+ }
+
+ @Test
+ public void name() {
+ assertThat(KURT).hasName("Kurt Alfred Kluever");
+ expectFailure(whenTesting -> whenTesting.that(KURT).hasName("Sundar Pichai"));
+ }
+
+ @Test
+ public void username() {
+ assertThat(KURT).hasUsername("kak");
+ // Here's an example of asserting on the failure message.
+ // Note that it uses the assertThat method from ExpectFailure.
+ AssertionError failure =
+ expectFailure(whenTesting -> whenTesting.that(KURT).hasUsername("sundar"));
+ assertThat(failure).factValue("value of").isEqualTo("employee.username()");
+ }
+
+ private static AssertionError expectFailure(
+ SimpleSubjectBuilderCallback<EmployeeSubject, Employee> callback) {
+ return expectFailureAbout(employees(), callback);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/extension/FakeHrDatabase.java b/core/src/test/java/com/google/common/truth/extension/FakeHrDatabase.java
new file mode 100644
index 00000000..78236b49
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/extension/FakeHrDatabase.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2017 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extension;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.truth.extension.Employee.Location;
+import java.util.HashMap;
+import java.util.Map;
+
+/** In-memory implementation of {@link HrDatabase}, suitable for testing. */
+public final class FakeHrDatabase implements HrDatabase {
+ private final Map<Long, Employee> employees = new HashMap<>();
+
+ public void put(Employee employee) {
+ employees.put(employee.id(), employee);
+ }
+
+ @Override
+ public Employee get(long id) {
+ return employees.get(id);
+ }
+
+ @Override
+ public void relocate(long id, Location location) {
+ checkNotNull(location);
+ Employee old = get(id);
+ checkState(old != null, "No employee found with ID %s", id);
+ employees.put(id, Employee.create(old.username(), old.id(), old.name(), location, old.isCeo()));
+ }
+
+ @Override
+ public ImmutableSet<Employee> getByLocation(Location location) {
+ checkNotNull(location);
+ ImmutableSet.Builder<Employee> result = ImmutableSet.builder();
+ for (Employee employee : employees.values()) {
+ if (employee.location() == location) {
+ result.add(employee);
+ }
+ }
+ return result.build();
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/extension/FakeHrDatabaseTest.java b/core/src/test/java/com/google/common/truth/extension/FakeHrDatabaseTest.java
new file mode 100644
index 00000000..d23c254d
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/extension/FakeHrDatabaseTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2015 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extension;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extension.EmployeeSubject.assertThat;
+
+import com.google.common.truth.extension.Employee.Location;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class FakeHrDatabaseTest {
+
+ // Note: not real employee IDs :-)
+
+ private static final Employee KURT =
+ Employee.create("kak", 37802, "Kurt Alfred Kluever", Location.NYC, false);
+
+ private static final Employee SUNDAR =
+ Employee.create("sundar", 5243, "Sundar Pichai", Location.MTV, true);
+
+ // Notice that we static import two different assertThat methods.
+
+ // These assertions use the EmployeeSubject.assertThat(Employee) overload and the
+ // EmployeeSubject-specific methods.
+
+ @Test
+ public void relocatePresent() {
+ FakeHrDatabase db = new FakeHrDatabase();
+ db.put(KURT);
+ db.relocate(KURT.id(), Location.MTV);
+ Employee movedKurt = db.get(KURT.id());
+ assertThat(movedKurt).hasLocation(Location.MTV);
+ assertThat(movedKurt).hasUsername("kak");
+ }
+
+ // These assertions use the EmployeeSubject.assertThat(Employee) overload but the assertion
+ // methods inherited from Subject.
+
+ @Test
+ public void getPresent() {
+ FakeHrDatabase db = new FakeHrDatabase();
+ db.put(KURT);
+ assertThat(db.get(KURT.id())).isEqualTo(KURT);
+ }
+
+ @Test
+ public void getAbsent() {
+ FakeHrDatabase db = new FakeHrDatabase();
+ db.put(KURT);
+ assertThat(db.get(SUNDAR.id())).isNull();
+ }
+
+ // These assertions use Truth.assertThat() overloads
+
+ @Test
+ public void getByLocation() {
+ FakeHrDatabase db = new FakeHrDatabase();
+ db.put(KURT);
+ assertThat(db.getByLocation(Location.NYC)).containsExactly(KURT);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/extension/HrDatabase.java b/core/src/test/java/com/google/common/truth/extension/HrDatabase.java
new file mode 100644
index 00000000..50a6a1dc
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/extension/HrDatabase.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2017 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extension;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.truth.extension.Employee.Location;
+
+/**
+ * Service for accessing human resources data.
+ *
+ * <p>This class (and all the classes in this package) are just a demonstration of how to write and
+ * use a custom Truth subject. The only implementation of {@code HrDatabase} is {@link
+ * FakeHrDatabase}, and the only place we use it is in its own test, whose real purpose is to
+ * demonstrate how to use a custom Truth subject.
+ */
+public interface HrDatabase {
+ Employee get(long id);
+
+ void relocate(long id, Location location);
+
+ ImmutableSet<Employee> getByLocation(Location location);
+}
diff --git a/core/src/test/java/com/google/common/truth/gwt/Inventory.java b/core/src/test/java/com/google/common/truth/gwt/Inventory.java
new file mode 100644
index 00000000..49006f98
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/gwt/Inventory.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.gwt;
+
+import com.google.common.truth.BigDecimalSubject;
+import com.google.common.truth.BooleanSubject;
+import com.google.common.truth.ClassSubject;
+import com.google.common.truth.ComparableSubject;
+import com.google.common.truth.DoubleSubject;
+import com.google.common.truth.FailureStrategy;
+import com.google.common.truth.FloatSubject;
+import com.google.common.truth.GuavaOptionalSubject;
+import com.google.common.truth.IntegerSubject;
+import com.google.common.truth.IterableSubject;
+import com.google.common.truth.LongSubject;
+import com.google.common.truth.MapSubject;
+import com.google.common.truth.MultimapSubject;
+import com.google.common.truth.MultisetSubject;
+import com.google.common.truth.ObjectArraySubject;
+import com.google.common.truth.Ordered;
+import com.google.common.truth.PrimitiveBooleanArraySubject;
+import com.google.common.truth.PrimitiveByteArraySubject;
+import com.google.common.truth.PrimitiveCharArraySubject;
+import com.google.common.truth.PrimitiveDoubleArraySubject;
+import com.google.common.truth.PrimitiveFloatArraySubject;
+import com.google.common.truth.PrimitiveIntArraySubject;
+import com.google.common.truth.PrimitiveLongArraySubject;
+import com.google.common.truth.PrimitiveShortArraySubject;
+import com.google.common.truth.StringSubject;
+import com.google.common.truth.Subject;
+import com.google.common.truth.TableSubject;
+import com.google.common.truth.ThrowableSubject;
+import com.google.common.truth.Truth;
+import com.google.common.truth.TruthJUnit;
+
+/**
+ * Static references to a variety of classes to force their loading during the {@link TruthGwtTest}.
+ */
+public class Inventory {
+ BigDecimalSubject bigDecimalSubject;
+ BooleanSubject booleanSubject;
+ ClassSubject classSubject;
+ ComparableSubject comparableSubject;
+ DoubleSubject doubleSubject;
+ FailureStrategy failureStrategy;
+ FloatSubject floatSubject;
+ GuavaOptionalSubject guavaOptionalSubject;
+ IntegerSubject integerSubject;
+ IterableSubject iterableSubject;
+ LongSubject longSubject;
+ MapSubject mapSubject;
+ MultimapSubject multimapSubject;
+ MultisetSubject multisetSubject;
+ ObjectArraySubject objectArraySubject;
+ Ordered ordered;
+ PrimitiveBooleanArraySubject primitiveBooleanArraySubject;
+ PrimitiveByteArraySubject primitiveByteArraySubject;
+ PrimitiveCharArraySubject primitiveCharArraySubject;
+ PrimitiveDoubleArraySubject primitiveDoubleArraySubject;
+ PrimitiveFloatArraySubject primitiveFloatArraySubject;
+ PrimitiveIntArraySubject primitiveIntArraySubject;
+ PrimitiveLongArraySubject primitiveLongArraySubject;
+ PrimitiveShortArraySubject primitiveShortArraySubject;
+ StringSubject stringSubject;
+ Subject.Factory subjectFactory;
+ Subject subject;
+ TableSubject tableSubject;
+ ThrowableSubject throwableSubject;
+ Truth truth;
+ TruthJUnit truthJUnit;
+}
diff --git a/core/src/test/java/com/google/common/truth/gwt/TruthGwtTest.java b/core/src/test/java/com/google/common/truth/gwt/TruthGwtTest.java
new file mode 100644
index 00000000..4a10e414
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/gwt/TruthGwtTest.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2011 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.gwt;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static java.util.Arrays.asList;
+
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Test of Truth under GWT - should be enough tests here to force compilation of all Subject
+ * implementations.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+
+public class TruthGwtTest extends com.google.gwt.junit.client.GWTTestCase {
+ @Override public String getModuleName() {
+ return "com.google.common.truth.gwt.TruthTest";
+ }
+
+ public void testBuildClasses() {
+ new Inventory().toString(); // force invocation.
+ }
+
+ public void testBoolean() {
+ assertThat(true).isTrue();
+ assertThat(false).isFalse();
+
+ try {
+ assertThat(true).isFalse();
+ } catch (AssertionError expected) {
+ return;
+ }
+ }
+
+ public void testInteger() {
+ assertThat(457923).isEqualTo(457923);
+ try {
+ assertThat(457923).isEqualTo(1);
+ } catch (AssertionError expected) {
+ return;
+ }
+ assert_().withMessage("Should have thrown an assertion error").fail();
+ }
+
+ public void testString() {
+ assertThat("blah").contains("ah");
+ assertThat("blah").startsWith("bl");
+ assertThat("blah").endsWith("ah");
+
+ try {
+ assertThat("blah").contains("foo");
+ } catch (AssertionError expected) {
+ return;
+ }
+ assert_().withMessage("Should have thrown an assertion error").fail();
+ }
+
+ public void testString_match() {
+ assertThat("blah").matches("b[la]+h");
+ assertThat("blah").matches("^b.+h$");
+ assertThat("blah").containsMatch("ah");
+ assertThat("blah").containsMatch("b[la]{2}h");
+ assertThat("blah").doesNotMatch("ah");
+ assertThat("blah").doesNotContainMatch("oh");
+ }
+
+ public void testString_matchesFail() {
+ try {
+ assertThat("blah").matches("b[lu]+h");
+ } catch (AssertionError expected) {
+ return;
+ }
+ assert_().withMessage("Should have thrown an assertion error").fail();
+ }
+
+ public void testString_containsMatchFail() {
+ try {
+ assertThat("blah").containsMatch("o");
+ } catch (AssertionError expected) {
+ return;
+ }
+ assert_().withMessage("Should have thrown an assertion error").fail();
+ }
+
+ public void testString_doesNotMatchFail() {
+ try {
+ assertThat("blah").doesNotMatch("blah");
+ } catch (AssertionError expected) {
+ return;
+ }
+ assert_().withMessage("Should have thrown an assertion error").fail();
+ }
+
+ public void testString_doesNotContainMatchFail() {
+ try {
+ assertThat("blah").doesNotContainMatch("a");
+ } catch (AssertionError expected) {
+ return;
+ }
+ assert_().withMessage("Should have thrown an assertion error").fail();
+ }
+
+ public void testIterable() {
+ assertThat((Iterable<Integer>) asList(1, 2, 3)).containsExactly(1, 2, 3).inOrder();
+ }
+
+ public void testCollection() {
+ assertThat((Collection<Integer>) asList(1, 2, 3)).containsExactly(1, 2, 3).inOrder();
+ }
+
+ public void testList() {
+ assertThat(asList(1, 2, 3)).containsExactly(1, 2, 3).inOrder();
+ }
+
+ public void testObjectArray() {
+ Set[] setOfString = {new HashSet<String>(asList("foo", "bar", "bash"))};
+ assertThat(setOfString).asList().contains(new HashSet<String>(asList("foo", "bar", "bash")));
+ }
+
+ public void testDefault() {
+ assertThat(new Object()).isNotNull();
+ assertThat(new ArrayList<String>()).isInstanceOf(AbstractList.class);
+ }
+
+ public void testLegacyAssert_() {
+ assert_().that(new Object()).isNotNull();
+ assert_().that(new ArrayList<String>()).isInstanceOf(AbstractList.class);
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/gwt/TruthTest.gwt.xml b/core/src/test/java/com/google/common/truth/gwt/TruthTest.gwt.xml
new file mode 100644
index 00000000..ee9caccb
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/gwt/TruthTest.gwt.xml
@@ -0,0 +1,8 @@
+<module>
+ <inherits name="com.google.common.base.Base"/>
+ <inherits name="com.google.common.collect.Collect"/>
+ <inherits name="com.google.common.primitives.Primitives"/>
+ <inherits name="com.google.common.truth.Truth"/>
+ <!-- TODO(cpovirk): Test Truth8. -->
+ <inherits name="com.google.gwt.junit.JUnit"/>
+</module>
diff --git a/core/src/test/java/com/google/common/truth/no_junit_test.sh b/core/src/test/java/com/google/common/truth/no_junit_test.sh
new file mode 100755
index 00000000..3a0e3f38
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/no_junit_test.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+set -euE
+
+# Just run the provided java_binary.
+"$1"
diff --git a/core/src/test/java/com/google/common/truth/super/com/google/common/truth/PlatformBaseSubjectTestCase.java b/core/src/test/java/com/google/common/truth/super/com/google/common/truth/PlatformBaseSubjectTestCase.java
new file mode 100644
index 00000000..1fdd3971
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/super/com/google/common/truth/PlatformBaseSubjectTestCase.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2017 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import org.junit.After;
+import org.junit.Before;
+
+public abstract class PlatformBaseSubjectTestCase {
+
+ final ExpectFailure expectFailure = new ExpectFailure();
+
+ @Before
+ public void setupExpectFailure() {
+ expectFailure.enterRuleContext(); // safe since @After forces leaving the context
+ }
+
+ @After
+ public void ensureExpectedFailureCaught() {
+ expectFailure.leaveRuleContext();
+ expectFailure.ensureFailureCaught();
+ }
+}
diff --git a/core/src/test/java/com/google/common/truth/super/com/google/common/truth/TestPlatform.java b/core/src/test/java/com/google/common/truth/super/com/google/common/truth/TestPlatform.java
new file mode 100644
index 00000000..b9263871
--- /dev/null
+++ b/core/src/test/java/com/google/common/truth/super/com/google/common/truth/TestPlatform.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2018 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+final class TestPlatform {
+ static boolean isGwt() {
+ return true;
+ }
+}
diff --git a/extensions/java8/pom.xml b/extensions/java8/pom.xml
new file mode 100644
index 00000000..f3dfec9f
--- /dev/null
+++ b/extensions/java8/pom.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>com.google.truth.extensions</groupId>
+ <artifactId>truth-extensions-parent</artifactId>
+ <version>HEAD-SNAPSHOT</version>
+ </parent>
+ <artifactId>truth-java8-extension</artifactId>
+ <name>Truth Extension for Java8</name>
+ <description>
+ An extension for the Truth test assertion framework supporting Java8 types and structures
+ </description>
+ <dependencies>
+ <dependency>
+ <groupId>com.google.truth</groupId>
+ <artifactId>truth</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.checkerframework</groupId>
+ <artifactId>checker-qual</artifactId>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>1.8</source>
+ <target>1.8</target>
+ </configuration>
+ </plugin>
+ <plugin>
+ <artifactId>maven-jar-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>attach-gwt-sources</id>
+ <phase>post-integration-test</phase>
+ <goals><goal>jar</goal></goals>
+ <configuration>
+ <classifier>gwt</classifier>
+ <classesDirectory>src/main/java</classesDirectory>
+ <includes>
+ <include>**/*.java</include>
+ <include>**/*.gwt.xml</include>
+ </includes>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
+
+
diff --git a/extensions/java8/src/main/java/com/google/common/truth/IntStreamSubject.java b/extensions/java8/src/main/java/com/google/common/truth/IntStreamSubject.java
new file mode 100644
index 00000000..cd898d07
--- /dev/null
+++ b/extensions/java8/src/main/java/com/google/common/truth/IntStreamSubject.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static java.util.stream.Collectors.toCollection;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Propositions for {@link IntStream} subjects.
+ *
+ * <p><b>Note:</b> the wrapped stream will be drained immediately into a private collection to
+ * provide more readable failure messages. You should not use this class if you intend to leave the
+ * stream un-consumed or if the stream is <i>very</i> large or infinite.
+ *
+ * <p>If you intend to make multiple assertions on the same stream of data you should instead first
+ * collect the contents of the stream into a collection, and then assert directly on that.
+ *
+ * <p>For very large or infinite streams you may want to first {@linkplain Stream#limit limit} the
+ * stream before asserting on it.
+ *
+ * @author Kurt Alfred Kluever
+ */
+public final class IntStreamSubject extends Subject {
+
+ private final List<?> actualList;
+
+ private IntStreamSubject(FailureMetadata failureMetadata, @Nullable IntStream stream) {
+ super(failureMetadata, stream);
+ this.actualList =
+ (stream == null) ? null : stream.boxed().collect(toCollection(ArrayList::new));
+ }
+
+ @Override
+ protected String actualCustomStringRepresentation() {
+ return String.valueOf(actualList);
+ }
+
+ public static Factory<IntStreamSubject, IntStream> intStreams() {
+ return IntStreamSubject::new;
+ }
+
+ /** Fails if the subject is not empty. */
+ public void isEmpty() {
+ check().that(actualList).isEmpty();
+ }
+
+ /** Fails if the subject is empty. */
+ public void isNotEmpty() {
+ check().that(actualList).isNotEmpty();
+ }
+
+ /**
+ * Fails if the subject does not have the given size.
+ *
+ * <p>If you'd like to check that your stream contains more than {@link Integer#MAX_VALUE}
+ * elements, use {@code assertThat(stream.count()).isEqualTo(...)}.
+ */
+ public void hasSize(int expectedSize) {
+ check().that(actualList).hasSize(expectedSize);
+ }
+
+ /** Fails if the subject does not contain the given element. */
+ public void contains(int element) {
+ check().that(actualList).contains(element);
+ }
+
+ /** Fails if the subject contains the given element. */
+ public void doesNotContain(int element) {
+ check().that(actualList).doesNotContain(element);
+ }
+
+ /** Fails if the subject contains duplicate elements. */
+ public void containsNoDuplicates() {
+ check().that(actualList).containsNoDuplicates();
+ }
+
+ /** Fails if the subject does not contain at least one of the given elements. */
+ @SuppressWarnings("GoodTime") // false positive; b/122617528
+ public void containsAnyOf(int first, int second, int... rest) {
+ check().that(actualList).containsAnyOf(first, second, box(rest));
+ }
+
+ /** Fails if the subject does not contain at least one of the given elements. */
+ public void containsAnyIn(Iterable<?> expected) {
+ check().that(actualList).containsAnyIn(expected);
+ }
+
+ /**
+ * Fails if the subject does not contain all of the given elements. If an element appears more
+ * than once in the given elements, then it must appear at least that number of times in the
+ * actual elements.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method. The expected elements must appear in the given order
+ * within the actual elements, but they are not required to be consecutive.
+ */
+ @SuppressWarnings("GoodTime") // false positive; b/122617528
+ @CanIgnoreReturnValue
+ public Ordered containsAtLeast(int first, int second, int... rest) {
+ return check().that(actualList).containsAtLeast(first, second, box(rest));
+ }
+
+ /**
+ * Fails if the subject does not contain all of the given elements. If an element appears more
+ * than once in the given elements, then it must appear at least that number of times in the
+ * actual elements.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method. The expected elements must appear in the given order
+ * within the actual elements, but they are not required to be consecutive.
+ */
+ @CanIgnoreReturnValue
+ public Ordered containsAtLeastElementsIn(Iterable<?> expected) {
+ return check().that(actualList).containsAtLeastElementsIn(expected);
+ }
+
+ /**
+ * Fails if the subject does not contain exactly the given elements.
+ *
+ * <p>Multiplicity is respected. For example, an object duplicated exactly 3 times in the
+ * parameters asserts that the object must likewise be duplicated exactly 3 times in the subject.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method.
+ */
+ @CanIgnoreReturnValue
+ public Ordered containsExactly(int... varargs) {
+ return check().that(actualList).containsExactly(box(varargs));
+ }
+
+ /**
+ * Fails if the subject does not contain exactly the given elements.
+ *
+ * <p>Multiplicity is respected. For example, an object duplicated exactly 3 times in the
+ * parameters asserts that the object must likewise be duplicated exactly 3 times in the subject.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method.
+ */
+ @CanIgnoreReturnValue
+ public Ordered containsExactlyElementsIn(Iterable<?> expected) {
+ return check().that(actualList).containsExactlyElementsIn(expected);
+ }
+
+ /**
+ * Fails if the subject contains any of the given elements. (Duplicates are irrelevant to this
+ * test, which fails if any of the actual elements equal any of the excluded.)
+ */
+ @SuppressWarnings("GoodTime") // false positive; b/122617528
+ public void containsNoneOf(int first, int second, int... rest) {
+ check().that(actualList).containsNoneOf(first, second, box(rest));
+ }
+
+ /**
+ * Fails if the subject contains any of the given elements. (Duplicates are irrelevant to this
+ * test, which fails if any of the actual elements equal any of the excluded.)
+ */
+ public void containsNoneIn(Iterable<?> excluded) {
+ check().that(actualList).containsNoneIn(excluded);
+ }
+
+ /**
+ * Fails if the subject is not strictly ordered, according to the natural ordering of its
+ * elements. Strictly ordered means that each element in the stream is <i>strictly</i> greater
+ * than the element that preceded it.
+ *
+ * @throws ClassCastException if any pair of elements is not mutually Comparable
+ * @throws NullPointerException if any element is null
+ */
+ public void isInStrictOrder() {
+ check().that(actualList).isInStrictOrder();
+ }
+
+ /**
+ * Fails if the subject is not strictly ordered, according to the given comparator. Strictly
+ * ordered means that each element in the stream is <i>strictly</i> greater than the element that
+ * preceded it.
+ *
+ * @throws ClassCastException if any pair of elements is not mutually Comparable
+ */
+ public void isInStrictOrder(Comparator<? super Integer> comparator) {
+ check().that(actualList).isInStrictOrder(comparator);
+ }
+
+ /**
+ * Fails if the subject is not ordered, according to the natural ordering of its elements. Ordered
+ * means that each element in the stream is greater than or equal to the element that preceded it.
+ *
+ * @throws ClassCastException if any pair of elements is not mutually Comparable
+ * @throws NullPointerException if any element is null
+ */
+ public void isInOrder() {
+ check().that(actualList).isInOrder();
+ }
+
+ /**
+ * Fails if the subject is not ordered, according to the given comparator. Ordered means that each
+ * element in the stream is greater than or equal to the element that preceded it.
+ *
+ * @throws ClassCastException if any pair of elements is not mutually Comparable
+ */
+ public void isInOrder(Comparator<? super Integer> comparator) {
+ check().that(actualList).isInOrder(comparator);
+ }
+
+ private static Object[] box(int[] rest) {
+ return IntStream.of(rest).boxed().toArray(Integer[]::new);
+ }
+
+ // TODO(user): Do we want to override + deprecate isEqualTo/isNotEqualTo?
+
+ // TODO(user): Do we want to support comparingElementsUsing() on StreamSubject?
+}
diff --git a/extensions/java8/src/main/java/com/google/common/truth/LongStreamSubject.java b/extensions/java8/src/main/java/com/google/common/truth/LongStreamSubject.java
new file mode 100644
index 00000000..1e6b5029
--- /dev/null
+++ b/extensions/java8/src/main/java/com/google/common/truth/LongStreamSubject.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static java.util.stream.Collectors.toCollection;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.LongStream;
+import java.util.stream.Stream;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Propositions for {@link LongStream} subjects.
+ *
+ * <p><b>Note:</b> the wrapped stream will be drained immediately into a private collection to
+ * provide more readable failure messages. You should not use this class if you intend to leave the
+ * stream un-consumed or if the stream is <i>very</i> large or infinite.
+ *
+ * <p>If you intend to make multiple assertions on the same stream of data you should instead first
+ * collect the contents of the stream into a collection, and then assert directly on that.
+ *
+ * <p>For very large or infinite streams you may want to first {@linkplain Stream#limit limit} the
+ * stream before asserting on it.
+ *
+ * @author Kurt Alfred Kluever
+ */
+public final class LongStreamSubject extends Subject {
+
+ private final List<?> actualList;
+
+ private LongStreamSubject(FailureMetadata failureMetadata, @Nullable LongStream stream) {
+ super(failureMetadata, stream);
+ this.actualList =
+ (stream == null) ? null : stream.boxed().collect(toCollection(ArrayList::new));
+ }
+
+ @Override
+ protected String actualCustomStringRepresentation() {
+ return String.valueOf(actualList);
+ }
+
+ public static Factory<LongStreamSubject, LongStream> longStreams() {
+ return LongStreamSubject::new;
+ }
+
+ /** Fails if the subject is not empty. */
+ public void isEmpty() {
+ check().that(actualList).isEmpty();
+ }
+
+ /** Fails if the subject is empty. */
+ public void isNotEmpty() {
+ check().that(actualList).isNotEmpty();
+ }
+
+ /**
+ * Fails if the subject does not have the given size.
+ *
+ * <p>If you'd like to check that your stream contains more than {@link Integer#MAX_VALUE}
+ * elements, use {@code assertThat(stream.count()).isEqualTo(...)}.
+ */
+ public void hasSize(int expectedSize) {
+ check().that(actualList).hasSize(expectedSize);
+ }
+
+ /** Fails if the subject does not contain the given element. */
+ public void contains(long element) {
+ check().that(actualList).contains(element);
+ }
+
+ /** Fails if the subject contains the given element. */
+ public void doesNotContain(long element) {
+ check().that(actualList).doesNotContain(element);
+ }
+
+ /** Fails if the subject contains duplicate elements. */
+ public void containsNoDuplicates() {
+ check().that(actualList).containsNoDuplicates();
+ }
+
+ /** Fails if the subject does not contain at least one of the given elements. */
+ @SuppressWarnings("GoodTime") // false positive; b/122617528
+ public void containsAnyOf(long first, long second, long... rest) {
+ check().that(actualList).containsAnyOf(first, second, box(rest));
+ }
+
+ /** Fails if the subject does not contain at least one of the given elements. */
+ public void containsAnyIn(Iterable<?> expected) {
+ check().that(actualList).containsAnyIn(expected);
+ }
+
+ /**
+ * Fails if the subject does not contain all of the given elements. If an element appears more
+ * than once in the given elements, then it must appear at least that number of times in the
+ * actual elements.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method. The expected elements must appear in the given order
+ * within the actual elements, but they are not required to be consecutive.
+ */
+ @SuppressWarnings("GoodTime") // false positive; b/122617528
+ @CanIgnoreReturnValue
+ public Ordered containsAtLeast(long first, long second, long... rest) {
+ return check().that(actualList).containsAtLeast(first, second, box(rest));
+ }
+
+ /**
+ * Fails if the subject does not contain all of the given elements. If an element appears more
+ * than once in the given elements, then it must appear at least that number of times in the
+ * actual elements.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method. The expected elements must appear in the given order
+ * within the actual elements, but they are not required to be consecutive.
+ */
+ @CanIgnoreReturnValue
+ public Ordered containsAtLeastElementsIn(Iterable<?> expected) {
+ return check().that(actualList).containsAtLeastElementsIn(expected);
+ }
+
+ /**
+ * Fails if the subject does not contain exactly the given elements.
+ *
+ * <p>Multiplicity is respected. For example, an object duplicated exactly 3 times in the
+ * parameters asserts that the object must likewise be duplicated exactly 3 times in the subject.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method.
+ */
+ @CanIgnoreReturnValue
+ public Ordered containsExactly(long... varargs) {
+ return check().that(actualList).containsExactly(box(varargs));
+ }
+
+ /**
+ * Fails if the subject does not contain exactly the given elements.
+ *
+ * <p>Multiplicity is respected. For example, an object duplicated exactly 3 times in the
+ * parameters asserts that the object must likewise be duplicated exactly 3 times in the subject.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method.
+ */
+ @CanIgnoreReturnValue
+ public Ordered containsExactlyElementsIn(Iterable<?> expected) {
+ return check().that(actualList).containsExactlyElementsIn(expected);
+ }
+
+ /**
+ * Fails if the subject contains any of the given elements. (Duplicates are irrelevant to this
+ * test, which fails if any of the actual elements equal any of the excluded.)
+ */
+ @SuppressWarnings("GoodTime") // false positive; b/122617528
+ public void containsNoneOf(long first, long second, long... rest) {
+ check().that(actualList).containsNoneOf(first, second, box(rest));
+ }
+
+ /**
+ * Fails if the subject contains any of the given elements. (Duplicates are irrelevant to this
+ * test, which fails if any of the actual elements equal any of the excluded.)
+ */
+ public void containsNoneIn(Iterable<?> excluded) {
+ check().that(actualList).containsNoneIn(excluded);
+ }
+
+ /**
+ * Fails if the subject is not strictly ordered, according to the natural ordering of its
+ * elements. Strictly ordered means that each element in the stream is <i>strictly</i> greater
+ * than the element that preceded it.
+ *
+ * @throws ClassCastException if any pair of elements is not mutually Comparable
+ * @throws NullPointerException if any element is null
+ */
+ public void isInStrictOrder() {
+ check().that(actualList).isInStrictOrder();
+ }
+
+ /**
+ * Fails if the subject is not strictly ordered, according to the given comparator. Strictly
+ * ordered means that each element in the stream is <i>strictly</i> greater than the element that
+ * preceded it.
+ *
+ * @throws ClassCastException if any pair of elements is not mutually Comparable
+ */
+ public void isInStrictOrder(Comparator<? super Long> comparator) {
+ check().that(actualList).isInStrictOrder(comparator);
+ }
+
+ /**
+ * Fails if the subject is not ordered, according to the natural ordering of its elements. Ordered
+ * means that each element in the stream is greater than or equal to the element that preceded it.
+ *
+ * @throws ClassCastException if any pair of elements is not mutually Comparable
+ * @throws NullPointerException if any element is null
+ */
+ public void isInOrder() {
+ check().that(actualList).isInOrder();
+ }
+
+ /**
+ * Fails if the subject is not ordered, according to the given comparator. Ordered means that each
+ * element in the stream is greater than or equal to the element that preceded it.
+ *
+ * @throws ClassCastException if any pair of elements is not mutually Comparable
+ */
+ public void isInOrder(Comparator<? super Long> comparator) {
+ check().that(actualList).isInOrder(comparator);
+ }
+
+ private static Object[] box(long[] rest) {
+ return LongStream.of(rest).boxed().toArray(Long[]::new);
+ }
+
+ // TODO(user): Do we want to override + deprecate isEqualTo/isNotEqualTo?
+
+ // TODO(user): Do we want to support comparingElementsUsing() on StreamSubject?
+}
diff --git a/extensions/java8/src/main/java/com/google/common/truth/OptionalDoubleSubject.java b/extensions/java8/src/main/java/com/google/common/truth/OptionalDoubleSubject.java
new file mode 100644
index 00000000..4a9aa5f8
--- /dev/null
+++ b/extensions/java8/src/main/java/com/google/common/truth/OptionalDoubleSubject.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.Fact.simpleFact;
+
+import java.util.OptionalDouble;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Propositions for Java 8 {@link OptionalDouble} subjects.
+ *
+ * @author Ben Douglass
+ */
+public final class OptionalDoubleSubject extends Subject {
+
+ private final OptionalDouble actual;
+
+ OptionalDoubleSubject(
+ FailureMetadata failureMetadata,
+ @Nullable OptionalDouble subject,
+ @Nullable String typeDescription) {
+ super(failureMetadata, subject, typeDescription);
+ this.actual = subject;
+ }
+
+ /** Fails if the {@link OptionalDouble} is empty or the subject is null. */
+ public void isPresent() {
+ if (actual == null) {
+ failWithActual(simpleFact("expected present optional"));
+ } else if (!actual.isPresent()) {
+ failWithoutActual(simpleFact("expected to be present"));
+ }
+ }
+
+ /** Fails if the {@link OptionalDouble} is present or the subject is null. */
+ public void isEmpty() {
+ if (actual == null) {
+ failWithActual(simpleFact("expected empty optional"));
+ } else if (actual.isPresent()) {
+ failWithoutActual(
+ simpleFact("expected to be empty"),
+ fact("but was present with value", actual.getAsDouble()));
+ }
+ }
+
+ /**
+ * Fails if the {@link OptionalDouble} does not have the given value or the subject is null. This
+ * method is <i>not</i> recommended when the code under test is doing any kind of arithmetic,
+ * since the exact result of floating point arithmetic is sensitive to apparently trivial changes.
+ * More sophisticated comparisons can be done using {@code assertThat(optional.getAsDouble())
}.
+ * This method is recommended when the code under test is specified as either copying a value
+ * without modification from its input or returning a well-defined literal or constant value.
+ */
+ public void hasValue(double expected) {
+ if (actual == null) {
+ failWithActual("expected an optional with value", expected);
+ } else if (!actual.isPresent()) {
+ failWithoutActual(fact("expected to have value", expected), simpleFact("but was absent"));
+ } else {
+ checkNoNeedToDisplayBothValues("getAsDouble()")
+ .that(actual.getAsDouble())
+ .isEqualTo(expected);
+ }
+ }
+
+ public static Subject.Factory<OptionalDoubleSubject, OptionalDouble> optionalDoubles() {
+ return (metadata, subject) -> new OptionalDoubleSubject(metadata, subject, "optionalDouble");
+ }
+}
diff --git a/extensions/java8/src/main/java/com/google/common/truth/OptionalIntSubject.java b/extensions/java8/src/main/java/com/google/common/truth/OptionalIntSubject.java
new file mode 100644
index 00000000..957594f7
--- /dev/null
+++ b/extensions/java8/src/main/java/com/google/common/truth/OptionalIntSubject.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.Fact.simpleFact;
+
+import java.util.OptionalInt;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Propositions for Java 8 {@link OptionalInt} subjects.
+ *
+ * @author Ben Douglass
+ */
+public final class OptionalIntSubject extends Subject {
+ private final OptionalInt actual;
+
+ OptionalIntSubject(
+ FailureMetadata failureMetadata,
+ @Nullable OptionalInt subject,
+ @Nullable String typeDescription) {
+ super(failureMetadata, subject, typeDescription);
+ this.actual = subject;
+ }
+
+ /** Fails if the {@link OptionalInt} is empty or the subject is null. */
+ public void isPresent() {
+ if (actual == null) {
+ failWithActual(simpleFact("expected present optional"));
+ } else if (!actual.isPresent()) {
+ failWithoutActual(simpleFact("expected to be present"));
+ }
+ }
+
+ /** Fails if the {@link OptionalInt} is present or the subject is null. */
+ public void isEmpty() {
+ if (actual == null) {
+ failWithActual(simpleFact("expected empty optional"));
+ } else if (actual.isPresent()) {
+ failWithoutActual(
+ simpleFact("expected to be empty"),
+ fact("but was present with value", actual.getAsInt()));
+ }
+ }
+
+ /**
+ * Fails if the {@link OptionalInt} does not have the given value or the subject is null. More
+ * sophisticated comparisons can be done using {@code assertThat(optional.getAsInt())
}.
+ */
+ public void hasValue(int expected) {
+ if (actual == null) {
+ failWithActual("expected an optional with value", expected);
+ } else if (!actual.isPresent()) {
+ failWithoutActual(fact("expected to have value", expected), simpleFact("but was absent"));
+ } else {
+ checkNoNeedToDisplayBothValues("getAsInt()").that(actual.getAsInt()).isEqualTo(expected);
+ }
+ }
+
+ public static Subject.Factory<OptionalIntSubject, OptionalInt> optionalInts() {
+ return (metadata, subject) -> new OptionalIntSubject(metadata, subject, "optionalInt");
+ }
+}
diff --git a/extensions/java8/src/main/java/com/google/common/truth/OptionalLongSubject.java b/extensions/java8/src/main/java/com/google/common/truth/OptionalLongSubject.java
new file mode 100644
index 00000000..9b57b9a4
--- /dev/null
+++ b/extensions/java8/src/main/java/com/google/common/truth/OptionalLongSubject.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.Fact.simpleFact;
+
+import java.util.OptionalLong;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Propositions for Java 8 {@link OptionalLong} subjects.
+ *
+ * @author Ben Douglass
+ */
+public final class OptionalLongSubject extends Subject {
+ private final OptionalLong actual;
+
+ OptionalLongSubject(
+ FailureMetadata failureMetadata,
+ @Nullable OptionalLong subject,
+ @Nullable String typeDescription) {
+ super(failureMetadata, subject, typeDescription);
+ this.actual = subject;
+ }
+
+ /** Fails if the {@link OptionalLong} is empty or the subject is null. */
+ public void isPresent() {
+ if (actual == null) {
+ failWithActual(simpleFact("expected present optional"));
+ } else if (!actual.isPresent()) {
+ failWithoutActual(simpleFact("expected to be present"));
+ }
+ }
+
+ /** Fails if the {@link OptionalLong} is present or the subject is null. */
+ public void isEmpty() {
+ if (actual == null) {
+ failWithActual(simpleFact("expected empty optional"));
+ } else if (actual.isPresent()) {
+ failWithoutActual(
+ simpleFact("expected to be empty"),
+ fact("but was present with value", actual.getAsLong()));
+ }
+ }
+
+ /**
+ * Fails if the {@link OptionalLong} does not have the given value or the subject is null. More
+ * sophisticated comparisons can be done using {@code assertThat(optional.getAsLong())
}.
+ */
+ public void hasValue(long expected) {
+ if (actual == null) {
+ failWithActual("expected an optional with value", expected);
+ } else if (!actual.isPresent()) {
+ failWithoutActual(fact("expected to have value", expected), simpleFact("but was absent"));
+ } else {
+ checkNoNeedToDisplayBothValues("getAsLong()").that(actual.getAsLong()).isEqualTo(expected);
+ }
+ }
+
+ public static Subject.Factory<OptionalLongSubject, OptionalLong> optionalLongs() {
+ return (metadata, subject) -> new OptionalLongSubject(metadata, subject, "optionalLong");
+ }
+}
diff --git a/extensions/java8/src/main/java/com/google/common/truth/OptionalSubject.java b/extensions/java8/src/main/java/com/google/common/truth/OptionalSubject.java
new file mode 100644
index 00000000..1b9fb5c9
--- /dev/null
+++ b/extensions/java8/src/main/java/com/google/common/truth/OptionalSubject.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.Fact.simpleFact;
+
+import java.util.Optional;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Propositions for Java 8 {@link Optional} subjects.
+ *
+ * @author Christian Gruber
+ */
+public final class OptionalSubject extends Subject {
+ private final Optional<?> actual;
+
+ OptionalSubject(
+ FailureMetadata failureMetadata,
+ @Nullable Optional<?> subject,
+ @Nullable String typeDescription) {
+ super(failureMetadata, subject, typeDescription);
+ this.actual = subject;
+ }
+
+ // TODO(cpovirk): Consider making OptionalIntSubject and OptionalLongSubject delegate to this.
+
+ /** Fails if the {@link Optional}{@code <T>} is empty or the subject is null. */
+ public void isPresent() {
+ if (actual == null) {
+ failWithActual(simpleFact("expected present optional"));
+ } else if (!actual.isPresent()) {
+ failWithoutActual(simpleFact("expected to be present"));
+ }
+ }
+
+ /** Fails if the {@link Optional}{@code <T>} is present or the subject is null. */
+ public void isEmpty() {
+ if (actual == null) {
+ failWithActual(simpleFact("expected empty optional"));
+ } else if (actual.isPresent()) {
+ failWithoutActual(
+ simpleFact("expected to be empty"), fact("but was present with value", actual.get()));
+ }
+ }
+
+ /**
+ * Fails if the {@link Optional}{@code <T>} does not have the given value or the subject is null.
+ *
+ * <p>To make more complex assertions on the optional's value split your assertion in two:
+ *
+ * <pre>{@code
+ * assertThat(myOptional).isPresent();
+ * assertThat(myOptional.get()).contains("foo");
+ * }</pre>
+ */
+ public void hasValue(Object expected) {
+ if (expected == null) {
+ throw new NullPointerException("Optional cannot have a null value.");
+ }
+ if (actual == null) {
+ failWithActual("expected an optional with value", expected);
+ } else if (!actual.isPresent()) {
+ failWithoutActual(fact("expected to have value", expected), simpleFact("but was empty"));
+ } else {
+ checkNoNeedToDisplayBothValues("get()").that(actual.get()).isEqualTo(expected);
+ }
+ }
+
+ public static Subject.Factory<OptionalSubject, Optional<?>> optionals() {
+ return (metadata, subject) -> new OptionalSubject(metadata, subject, "optional");
+ }
+}
diff --git a/extensions/java8/src/main/java/com/google/common/truth/PathSubject.java b/extensions/java8/src/main/java/com/google/common/truth/PathSubject.java
new file mode 100644
index 00000000..0be8532e
--- /dev/null
+++ b/extensions/java8/src/main/java/com/google/common/truth/PathSubject.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2017 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import com.google.common.annotations.GwtIncompatible;
+import com.google.j2objc.annotations.J2ObjCIncompatible;
+import java.nio.file.Path;
+
+/** Assertions for {@link Path} instances. */
+@GwtIncompatible
+@J2ObjCIncompatible
+public final class PathSubject extends Subject {
+ private PathSubject(FailureMetadata failureMetadata, Path actual) {
+ super(failureMetadata, actual);
+ }
+
+ public static Subject.Factory<PathSubject, Path> paths() {
+ return PathSubject::new;
+ }
+}
diff --git a/extensions/java8/src/main/java/com/google/common/truth/StreamSubject.java b/extensions/java8/src/main/java/com/google/common/truth/StreamSubject.java
new file mode 100644
index 00000000..4957686d
--- /dev/null
+++ b/extensions/java8/src/main/java/com/google/common/truth/StreamSubject.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static java.util.stream.Collectors.toCollection;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Stream;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Propositions for {@link Stream} subjects.
+ *
+ * <p><b>Note:</b> the wrapped stream will be drained immediately into a private collection to
+ * provide more readable failure messages. You should not use this class if you intend to leave the
+ * stream un-consumed or if the stream is <i>very</i> large or infinite.
+ *
+ * <p>If you intend to make multiple assertions on the same stream of data you should instead first
+ * collect the contents of the stream into a collection, and then assert directly on that.
+ *
+ * <p>For very large or infinite streams you may want to first {@linkplain Stream#limit limit} the
+ * stream before asserting on it.
+ *
+ * @author Kurt Alfred Kluever
+ */
+public final class StreamSubject extends Subject {
+
+ private final List<?> actualList;
+
+ private StreamSubject(FailureMetadata failureMetadata, @Nullable Stream<?> stream) {
+ super(failureMetadata, stream);
+ this.actualList = (stream == null) ? null : stream.collect(toCollection(ArrayList::new));
+ }
+
+ @Override
+ protected String actualCustomStringRepresentation() {
+ return String.valueOf(actualList);
+ }
+
+ public static Subject.Factory<StreamSubject, Stream<?>> streams() {
+ return (metadata, subject) -> new StreamSubject(metadata, subject);
+ }
+
+ /** Fails if the subject is not empty. */
+ public void isEmpty() {
+ check().that(actualList).isEmpty();
+ }
+
+ /** Fails if the subject is empty. */
+ public void isNotEmpty() {
+ check().that(actualList).isNotEmpty();
+ }
+
+ /**
+ * Fails if the subject does not have the given size.
+ *
+ * <p>If you'd like to check that your stream contains more than {@link Integer#MAX_VALUE}
+ * elements, use {@code assertThat(stream.count()).isEqualTo(...)}.
+ */
+ public void hasSize(int expectedSize) {
+ check().that(actualList).hasSize(expectedSize);
+ }
+
+ /** Fails if the subject does not contain the given element. */
+ public void contains(@Nullable Object element) {
+ check().that(actualList).contains(element);
+ }
+
+ /** Fails if the subject contains the given element. */
+ public void doesNotContain(@Nullable Object element) {
+ check().that(actualList).doesNotContain(element);
+ }
+
+ /** Fails if the subject contains duplicate elements. */
+ public void containsNoDuplicates() {
+ check().that(actualList).containsNoDuplicates();
+ }
+
+ /** Fails if the subject does not contain at least one of the given elements. */
+ public void containsAnyOf(
+ @Nullable Object first, @Nullable Object second, @Nullable Object @Nullable ... rest) {
+ check().that(actualList).containsAnyOf(first, second, rest);
+ }
+
+ /** Fails if the subject does not contain at least one of the given elements. */
+ public void containsAnyIn(Iterable<?> expected) {
+ check().that(actualList).containsAnyIn(expected);
+ }
+
+ /**
+ * Fails if the subject does not contain all of the given elements. If an element appears more
+ * than once in the given elements, then it must appear at least that number of times in the
+ * actual elements.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method. The expected elements must appear in the given order
+ * within the actual elements, but they are not required to be consecutive.
+ */
+ @CanIgnoreReturnValue
+ public Ordered containsAtLeast(
+ @Nullable Object first, @Nullable Object second, @Nullable Object @Nullable ... rest) {
+ return check().that(actualList).containsAtLeast(first, second, rest);
+ }
+
+ /**
+ * Fails if the subject does not contain all of the given elements. If an element appears more
+ * than once in the given elements, then it must appear at least that number of times in the
+ * actual elements.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method. The expected elements must appear in the given order
+ * within the actual elements, but they are not required to be consecutive.
+ */
+ @CanIgnoreReturnValue
+ public Ordered containsAtLeastElementsIn(Iterable<?> expected) {
+ return check().that(actualList).containsAtLeastElementsIn(expected);
+ }
+
+ // TODO(cpovirk): Add array overload of contains*ElementsIn methods? Also for int and long stream.
+
+ /**
+ * Fails if the subject does not contain exactly the given elements.
+ *
+ * <p>Multiplicity is respected. For example, an object duplicated exactly 3 times in the
+ * parameters asserts that the object must likewise be duplicated exactly 3 times in the subject.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method.
+ */
+ @CanIgnoreReturnValue
+ public Ordered containsExactly(@Nullable Object @Nullable ... varargs) {
+ return check().that(actualList).containsExactly(varargs);
+ }
+
+ /**
+ * Fails if the subject does not contain exactly the given elements.
+ *
+ * <p>Multiplicity is respected. For example, an object duplicated exactly 3 times in the
+ * parameters asserts that the object must likewise be duplicated exactly 3 times in the subject.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method.
+ */
+ @CanIgnoreReturnValue
+ public Ordered containsExactlyElementsIn(Iterable<?> expected) {
+ return check().that(actualList).containsExactlyElementsIn(expected);
+ }
+
+ /**
+ * Fails if the subject contains any of the given elements. (Duplicates are irrelevant to this
+ * test, which fails if any of the actual elements equal any of the excluded.)
+ */
+ public void containsNoneOf(
+ @Nullable Object first, @Nullable Object second, @Nullable Object @Nullable ... rest) {
+ check().that(actualList).containsNoneOf(first, second, rest);
+ }
+
+ /**
+ * Fails if the subject contains any of the given elements. (Duplicates are irrelevant to this
+ * test, which fails if any of the actual elements equal any of the excluded.)
+ */
+ public void containsNoneIn(Iterable<?> excluded) {
+ check().that(actualList).containsNoneIn(excluded);
+ }
+
+ /**
+ * Fails if the subject is not strictly ordered, according to the natural ordering of its
+ * elements. Strictly ordered means that each element in the stream is <i>strictly</i> greater
+ * than the element that preceded it.
+ *
+ * @throws ClassCastException if any pair of elements is not mutually Comparable
+ * @throws NullPointerException if any element is null
+ */
+ public void isInStrictOrder() {
+ check().that(actualList).isInStrictOrder();
+ }
+
+ /**
+ * Fails if the subject is not strictly ordered, according to the given comparator. Strictly
+ * ordered means that each element in the stream is <i>strictly</i> greater than the element that
+ * preceded it.
+ *
+ * @throws ClassCastException if any pair of elements is not mutually Comparable
+ */
+ public void isInStrictOrder(Comparator<?> comparator) {
+ check().that(actualList).isInStrictOrder(comparator);
+ }
+
+ /**
+ * Fails if the subject is not ordered, according to the natural ordering of its elements. Ordered
+ * means that each element in the stream is greater than or equal to the element that preceded it.
+ *
+ * @throws ClassCastException if any pair of elements is not mutually Comparable
+ * @throws NullPointerException if any element is null
+ */
+ public void isInOrder() {
+ check().that(actualList).isInOrder();
+ }
+
+ /**
+ * Fails if the subject is not ordered, according to the given comparator. Ordered means that each
+ * element in the stream is greater than or equal to the element that preceded it.
+ *
+ * @throws ClassCastException if any pair of elements is not mutually Comparable
+ */
+ public void isInOrder(Comparator<?> comparator) {
+ check().that(actualList).isInOrder(comparator);
+ }
+
+ // TODO(user): Do we want to override + deprecate isEqualTo/isNotEqualTo?
+
+ // TODO(user): Do we want to support comparingElementsUsing() on StreamSubject?
+}
diff --git a/extensions/java8/src/main/java/com/google/common/truth/Truth8.gwt.xml b/extensions/java8/src/main/java/com/google/common/truth/Truth8.gwt.xml
new file mode 100644
index 00000000..a3ac1443
--- /dev/null
+++ b/extensions/java8/src/main/java/com/google/common/truth/Truth8.gwt.xml
@@ -0,0 +1,28 @@
+<module>
+<source path="">
+ <!-- Hack to keep collect from hiding collect.testing supersource: -->
+ <exclude name="**/testing/**"/>
+</source>
+
+<!--
+ We used to set this only for packages that had manual supersource.
+ That worked everywhere that I know of except for one place:
+ when running the GWT util.concurrent tests under Guava.
+ The problem is that GWT responds poorly to two .gwt.xml files in the same Java package:
+ https://goo.gl/pRV3Yn
+ The summary is that it ignores one file in favor of the other.
+ util.concurrent, like nearly all our packages, has two .gwt.xml files: one for prod and one for tests.
+ util.concurrent, unlike our other packages, has, as of this writing, test supersource but no prod supersource.
+ GWT happens to use the prod .gwt.xml, so it looks for no supersource for tests, either.
+ This causes it to fail to find AtomicLongMapTest.
+ Our workaround is to tell GWT that util.concurrent and all other packages have prod supersource, even if they have none.
+ GWT is happy to ignore us when we specify a nonexistent path.
+ (I hope that this workaround does not cause its own problems in the future.)
+-->
+<super-source path="super"/>
+
+<inherits name="com.google.common.annotations.Annotations" />
+<inherits name="com.google.common.truth.Truth" />
+<inherits name="com.google.gwt.core.Core" />
+<inherits name="com.google.gwt.user.User" />
+</module>
diff --git a/extensions/java8/src/main/java/com/google/common/truth/Truth8.java b/extensions/java8/src/main/java/com/google/common/truth/Truth8.java
new file mode 100644
index 00000000..31f0c6d9
--- /dev/null
+++ b/extensions/java8/src/main/java/com/google/common/truth/Truth8.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth.assertAbout;
+
+import com.google.common.annotations.GwtIncompatible;
+import com.google.j2objc.annotations.J2ObjCIncompatible;
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.OptionalDouble;
+import java.util.OptionalInt;
+import java.util.OptionalLong;
+import java.util.stream.IntStream;
+import java.util.stream.LongStream;
+import java.util.stream.Stream;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * The primary entry point for assertions about Java 8 types.
+ *
+ * <p>To use {@link Truth#assertWithMessage} with a Java 8 type, use {@code
+ * assertWithMessage(...).about(}{@link OptionalSubject#optionals optionals()}{@code ).that(...)}
+ * (or similarly for the other types).
+ *
+ * <p>Likewise, to use different failure strategies like {@link Expect}, use {@code
+ * expect.about(}{@link OptionalSubject#optionals optionals()}{@code ).that(...)}.
+ *
+ * <p>For more information about combining different messages, failure strategies, and subjects, see
+ * <a href="https://truth.dev/faq#full-chain">How do I specify a custom message/failure
+ * behavior/{@code Subject} type?</a> in the Truth FAQ.
+ */
+public final class Truth8 {
+ public static OptionalSubject assertThat(@Nullable Optional<?> target) {
+ return assertAbout(OptionalSubject.optionals()).that(target);
+ }
+
+ public static OptionalIntSubject assertThat(@Nullable OptionalInt target) {
+ return assertAbout(OptionalIntSubject.optionalInts()).that(target);
+ }
+
+ public static OptionalLongSubject assertThat(@Nullable OptionalLong target) {
+ return assertAbout(OptionalLongSubject.optionalLongs()).that(target);
+ }
+
+ public static OptionalDoubleSubject assertThat(@Nullable OptionalDouble target) {
+ return assertAbout(OptionalDoubleSubject.optionalDoubles()).that(target);
+ }
+
+ public static StreamSubject assertThat(@Nullable Stream<?> target) {
+ return assertAbout(StreamSubject.streams()).that(target);
+ }
+
+ public static IntStreamSubject assertThat(@Nullable IntStream target) {
+ return assertAbout(IntStreamSubject.intStreams()).that(target);
+ }
+
+ public static LongStreamSubject assertThat(@Nullable LongStream target) {
+ return assertAbout(LongStreamSubject.longStreams()).that(target);
+ }
+
+ // TODO(b/64757353): Add support for DoubleStream?
+
+ // Not actually a Java 8 feature, but for now this is the best option since core Truth still has
+ // to support Java environments without java.nio.file such as Android and J2CL.
+ @GwtIncompatible
+ @J2ObjCIncompatible
+ public static PathSubject assertThat(@Nullable Path target) {
+ return assertAbout(PathSubject.paths()).that(target);
+ }
+
+ private Truth8() {}
+}
diff --git a/extensions/java8/src/test/java/com/google/common/truth/ExpectFailure8Test.java b/extensions/java8/src/test/java/com/google/common/truth/ExpectFailure8Test.java
new file mode 100644
index 00000000..197bfcf1
--- /dev/null
+++ b/extensions/java8/src/test/java/com/google/common/truth/ExpectFailure8Test.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2017 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.ExpectFailure.assertThat;
+import static com.google.common.truth.ExpectFailure.expectFailure;
+import static com.google.common.truth.ExpectFailure.expectFailureAbout;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.truth.ExpectFailure.SimpleSubjectBuilderCallback;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests of {@link ExpectFailure}'s Java 8 support. */
+@RunWith(JUnit4.class)
+public final class ExpectFailure8Test {
+
+ @Test
+ public void testExpectFailure() throws Exception {
+ AssertionError failure1 = expectFailure(whenTesting -> whenTesting.that(4).isEqualTo(5));
+ assertThat(failure1).factValue("expected").isEqualTo("5");
+
+ // verify multiple independent failures can be caught in the same test
+ AssertionError failure2 = expectFailure(whenTesting -> whenTesting.that(5).isEqualTo(4));
+ assertThat(failure2).factValue("expected").isEqualTo("4");
+ }
+
+ @Test
+ public void testExpectFailureAbout() {
+ AssertionError unused =
+ expectFailureAbout(
+ STRINGS,
+ (SimpleSubjectBuilderCallback<StringSubject, String>)
+ whenTesting -> whenTesting.that("foo").contains("bar"));
+ }
+
+ private static final Subject.Factory<StringSubject, String> STRINGS = StringSubject::new;
+}
diff --git a/extensions/java8/src/test/java/com/google/common/truth/FailureAssertions.java b/extensions/java8/src/test/java/com/google/common/truth/FailureAssertions.java
new file mode 100644
index 00000000..abdeb0ba
--- /dev/null
+++ b/extensions/java8/src/test/java/com/google/common/truth/FailureAssertions.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2017 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.ExpectFailure.assertThat;
+
+/** Convenience methods for Java 8 Truth Subject tests, similar to BaseSubjectTestCase. */
+final class FailureAssertions {
+ static void assertFailureKeys(AssertionError e, String... keys) {
+ assertThat(e).factKeys().containsExactlyElementsIn(keys).inOrder();
+ }
+
+ static void assertFailureValue(AssertionError e, String key, String value) {
+ assertThat(e).factValue(key).isEqualTo(value);
+ }
+
+ static void assertFailureValueIndexed(AssertionError e, String key, int index, String value) {
+ assertThat(e).factValue(key, index).isEqualTo(value);
+ }
+
+ private FailureAssertions() {}
+}
diff --git a/extensions/java8/src/test/java/com/google/common/truth/IntStreamSubjectTest.java b/extensions/java8/src/test/java/com/google/common/truth/IntStreamSubjectTest.java
new file mode 100644
index 00000000..bcb11bd9
--- /dev/null
+++ b/extensions/java8/src/test/java/com/google/common/truth/IntStreamSubjectTest.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.FailureAssertions.assertFailureKeys;
+import static com.google.common.truth.FailureAssertions.assertFailureValue;
+import static com.google.common.truth.IntStreamSubject.intStreams;
+import static com.google.common.truth.Truth8.assertThat;
+import static java.util.Arrays.asList;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+import java.util.stream.IntStream;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Java 8 {@link IntStream} Subjects.
+ *
+ * @author Kurt Alfred Kluever
+ */
+@RunWith(JUnit4.class)
+public final class IntStreamSubjectTest {
+
+ @Test
+ public void testIsEqualTo() throws Exception {
+ IntStream stream = IntStream.of(42);
+ assertThat(stream).isEqualTo(stream);
+ }
+
+ @Test
+ public void testIsEqualToList() throws Exception {
+ IntStream stream = IntStream.of(42);
+ List<Integer> list = asList(42);
+ AssertionError unused = expectFailure(whenTesting -> whenTesting.that(stream).isEqualTo(list));
+ }
+
+ @Test
+ public void testNullStream_fails() throws Exception {
+ IntStream nullStream = null;
+ try {
+ assertThat(nullStream).isEmpty();
+ fail();
+ } catch (NullPointerException expected) {
+ }
+ }
+
+ @Test
+ public void testNullStreamIsNull() throws Exception {
+ IntStream nullStream = null;
+ assertThat(nullStream).isNull();
+ }
+
+ @Test
+ public void testIsSameInstanceAs() throws Exception {
+ IntStream stream = IntStream.of(1);
+ assertThat(stream).isSameInstanceAs(stream);
+ }
+
+ @Test
+ public void testIsEmpty() throws Exception {
+ assertThat(IntStream.of()).isEmpty();
+ }
+
+ @Test
+ public void testIsEmpty_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(IntStream.of(42)).isEmpty());
+ }
+
+ @Test
+ public void testIsNotEmpty() throws Exception {
+ assertThat(IntStream.of(42)).isNotEmpty();
+ }
+
+ @Test
+ public void testIsNotEmpty_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(IntStream.of()).isNotEmpty());
+ }
+
+ @Test
+ public void testHasSize() throws Exception {
+ assertThat(IntStream.of(42)).hasSize(1);
+ }
+
+ @Test
+ public void testHasSize_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(IntStream.of(42)).hasSize(2));
+ }
+
+ @Test
+ public void testContainsNoDuplicates() throws Exception {
+ assertThat(IntStream.of(42)).containsNoDuplicates();
+ }
+
+ @Test
+ public void testContainsNoDuplicates_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(IntStream.of(42, 42)).containsNoDuplicates());
+ }
+
+ @Test
+ public void testContains() throws Exception {
+ assertThat(IntStream.of(42)).contains(42);
+ }
+
+ @Test
+ public void testContains_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(IntStream.of(42)).contains(100));
+ }
+
+ @Test
+ public void testContainsAnyOf() throws Exception {
+ assertThat(IntStream.of(42)).containsAnyOf(42, 43);
+ }
+
+ @Test
+ public void testContainsAnyOf_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(IntStream.of(42)).containsAnyOf(43, 44));
+ }
+
+ @Test
+ public void testContainsAnyIn() throws Exception {
+ assertThat(IntStream.of(42)).containsAnyIn(asList(42, 43));
+ }
+
+ @Test
+ public void testContainsAnyIn_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(
+ whenTesting -> whenTesting.that(IntStream.of(42)).containsAnyIn(asList(43, 44)));
+ }
+
+ @Test
+ public void testDoesNotContain() throws Exception {
+ assertThat(IntStream.of(42)).doesNotContain(43);
+ }
+
+ @Test
+ public void testDoesNotContain_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(IntStream.of(42)).doesNotContain(42));
+ }
+
+ @Test
+ public void testContainsNoneOf() throws Exception {
+ assertThat(IntStream.of(42)).containsNoneOf(43, 44);
+ }
+
+ @Test
+ public void testContainsNoneOf_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(IntStream.of(42)).containsNoneOf(42, 43));
+ }
+
+ @Test
+ public void testContainsNoneIn() throws Exception {
+ assertThat(IntStream.of(42)).containsNoneIn(asList(43, 44));
+ }
+
+ @Test
+ public void testContainsNoneIn_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(
+ whenTesting -> whenTesting.that(IntStream.of(42)).containsNoneIn(asList(42, 43)));
+ }
+
+ @Test
+ public void testContainsAtLeast() throws Exception {
+ assertThat(IntStream.of(42, 43)).containsAtLeast(42, 43);
+ }
+
+ @Test
+ public void testContainsAtLeast_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(
+ whenTesting -> whenTesting.that(IntStream.of(42, 43)).containsAtLeast(42, 43, 44));
+ }
+
+ @Test
+ public void testContainsAtLeast_inOrder() throws Exception {
+ assertThat(IntStream.of(42, 43)).containsAtLeast(42, 43).inOrder();
+ }
+
+ @Test
+ public void testContainsAtLeast_inOrder_fails() throws Exception {
+ try {
+ assertThat(IntStream.of(42, 43)).containsAtLeast(43, 42).inOrder();
+ fail();
+ } catch (AssertionError expected) {
+ assertFailureKeys(
+ expected,
+ "required elements were all found, but order was wrong",
+ "expected order for required elements",
+ "but was");
+ assertFailureValue(expected, "expected order for required elements", "[43, 42]");
+ }
+ }
+
+ @Test
+ public void testContainsAtLeastElementsIn() throws Exception {
+ assertThat(IntStream.of(42, 43)).containsAtLeastElementsIn(asList(42, 43));
+ }
+
+ @Test
+ public void testContainsAtLeastElementsIn_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(
+ whenTesting ->
+ whenTesting
+ .that(IntStream.of(42, 43))
+ .containsAtLeastElementsIn(asList(42, 43, 44)));
+ }
+
+ @Test
+ public void testContainsAtLeastElementsIn_inOrder() throws Exception {
+ assertThat(IntStream.of(42, 43)).containsAtLeastElementsIn(asList(42, 43)).inOrder();
+ }
+
+ @Test
+ public void testContainsAtLeastElementsIn_inOrder_fails() throws Exception {
+ try {
+ assertThat(IntStream.of(42, 43)).containsAtLeastElementsIn(asList(43, 42)).inOrder();
+ fail();
+ } catch (AssertionError expected) {
+ assertFailureKeys(
+ expected,
+ "required elements were all found, but order was wrong",
+ "expected order for required elements",
+ "but was");
+ assertFailureValue(expected, "expected order for required elements", "[43, 42]");
+ }
+ }
+
+ @Test
+ public void testContainsExactly() throws Exception {
+ assertThat(IntStream.of(42, 43)).containsExactly(42, 43);
+ }
+
+ @Test
+ public void testContainsExactly_fails() throws Exception {
+ try {
+ assertThat(IntStream.of(42, 43)).containsExactly(42);
+ fail();
+ } catch (AssertionError expected) {
+ assertFailureKeys(expected, "unexpected (1)", "---", "expected", "but was");
+ assertFailureValue(expected, "expected", "[42]");
+ }
+ }
+
+ @Test
+ public void testContainsExactly_inOrder() throws Exception {
+ assertThat(IntStream.of(42, 43)).containsExactly(42, 43).inOrder();
+ }
+
+ @Test
+ public void testContainsExactly_inOrder_fails() throws Exception {
+ try {
+ assertThat(IntStream.of(42, 43)).containsExactly(43, 42).inOrder();
+ fail();
+ } catch (AssertionError expected) {
+ assertFailureKeys(expected, "contents match, but order was wrong", "expected", "but was");
+ assertFailureValue(expected, "expected", "[43, 42]");
+ }
+ }
+
+ @Test
+ public void testContainsExactlyElementsIn() throws Exception {
+ assertThat(IntStream.of(42, 43)).containsExactlyElementsIn(asList(42, 43));
+ assertThat(IntStream.of(42, 43)).containsExactlyElementsIn(asList(43, 42));
+ }
+
+ @Test
+ public void testContainsExactlyElementsIn_fails() throws Exception {
+ try {
+ assertThat(IntStream.of(42, 43)).containsExactlyElementsIn(asList(42));
+ fail();
+ } catch (AssertionError expected) {
+ assertFailureKeys(expected, "unexpected (1)", "---", "expected", "but was");
+ assertFailureValue(expected, "expected", "[42]");
+ }
+ }
+
+ @Test
+ public void testContainsExactlyElementsIn_inOrder() throws Exception {
+ assertThat(IntStream.of(42, 43)).containsExactlyElementsIn(asList(42, 43)).inOrder();
+ }
+
+ @Test
+ public void testContainsExactlyElementsIn_inOrder_fails() throws Exception {
+ try {
+ assertThat(IntStream.of(42, 43)).containsExactlyElementsIn(asList(43, 42)).inOrder();
+ fail();
+ } catch (AssertionError expected) {
+ assertFailureKeys(expected, "contents match, but order was wrong", "expected", "but was");
+ assertFailureValue(expected, "expected", "[43, 42]");
+ }
+ }
+
+ @Test
+ public void testContainsExactlyElementsIn_inOrder_intStream() throws Exception {
+ assertThat(IntStream.of(1, 2, 3, 4)).containsExactly(1, 2, 3, 4).inOrder();
+ }
+
+ @Test
+ public void testIsInOrder() {
+ assertThat(IntStream.of()).isInOrder();
+ assertThat(IntStream.of(1)).isInOrder();
+ assertThat(IntStream.of(1, 1, 2, 3, 3, 3, 4)).isInOrder();
+ }
+
+ @Test
+ public void testIsInOrder_fails() {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(IntStream.of(1, 3, 2, 4)).isInOrder());
+ }
+
+ @Test
+ public void testIsInStrictOrder() {
+ assertThat(IntStream.of()).isInStrictOrder();
+ assertThat(IntStream.of(1)).isInStrictOrder();
+ assertThat(IntStream.of(1, 2, 3, 4)).isInStrictOrder();
+ }
+
+ @Test
+ public void testIsInStrictOrder_fails() {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(IntStream.of(1, 2, 2, 4)).isInStrictOrder());
+ }
+
+ private static AssertionError expectFailure(
+ ExpectFailure.SimpleSubjectBuilderCallback<IntStreamSubject, IntStream> assertionCallback) {
+ return ExpectFailure.expectFailureAbout(intStreams(), assertionCallback);
+ }
+}
diff --git a/extensions/java8/src/test/java/com/google/common/truth/LongStreamSubjectTest.java b/extensions/java8/src/test/java/com/google/common/truth/LongStreamSubjectTest.java
new file mode 100644
index 00000000..9908b354
--- /dev/null
+++ b/extensions/java8/src/test/java/com/google/common/truth/LongStreamSubjectTest.java
@@ -0,0 +1,394 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.FailureAssertions.assertFailureKeys;
+import static com.google.common.truth.FailureAssertions.assertFailureValue;
+import static com.google.common.truth.LongStreamSubject.longStreams;
+import static com.google.common.truth.Truth8.assertThat;
+import static java.util.Arrays.asList;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+import java.util.stream.LongStream;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Java 8 {@link LongStream} Subjects.
+ *
+ * @author Kurt Alfred Kluever
+ */
+@RunWith(JUnit4.class)
+public final class LongStreamSubjectTest {
+
+ @Test
+ public void testIsEqualTo() throws Exception {
+ LongStream stream = LongStream.of(42);
+ assertThat(stream).isEqualTo(stream);
+ }
+
+ @Test
+ public void testIsEqualToList() throws Exception {
+ LongStream stream = LongStream.of(42);
+ List<Long> list = asList(42L);
+ AssertionError unused = expectFailure(whenTesting -> whenTesting.that(stream).isEqualTo(list));
+ }
+
+ @Test
+ public void testNullStream_fails() throws Exception {
+ LongStream nullStream = null;
+ try {
+ assertThat(nullStream).isEmpty();
+ fail();
+ } catch (NullPointerException expected) {
+ }
+ }
+
+ @Test
+ public void testNullStreamIsNull() throws Exception {
+ LongStream nullStream = null;
+ assertThat(nullStream).isNull();
+ }
+
+ @Test
+ public void testIsSameInstanceAs() throws Exception {
+ LongStream stream = LongStream.of(1);
+ assertThat(stream).isSameInstanceAs(stream);
+ }
+
+ @Test
+ public void testIsEmpty() throws Exception {
+ assertThat(LongStream.of()).isEmpty();
+ }
+
+ @Test
+ public void testIsEmpty_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(LongStream.of(42)).isEmpty());
+ }
+
+ @Test
+ public void testIsNotEmpty() throws Exception {
+ assertThat(LongStream.of(42)).isNotEmpty();
+ }
+
+ @Test
+ public void testIsNotEmpty_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(LongStream.of()).isNotEmpty());
+ }
+
+ @Test
+ public void testHasSize() throws Exception {
+ assertThat(LongStream.of(42)).hasSize(1);
+ }
+
+ @Test
+ public void testHasSize_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(LongStream.of(42)).hasSize(2));
+ }
+
+ @Test
+ public void testContainsNoDuplicates() throws Exception {
+ assertThat(LongStream.of(42)).containsNoDuplicates();
+ }
+
+ @Test
+ public void testContainsNoDuplicates_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(
+ whenTesting -> whenTesting.that(LongStream.of(42, 42)).containsNoDuplicates());
+ }
+
+ @Test
+ public void testContains() throws Exception {
+ assertThat(LongStream.of(42)).contains(42);
+ }
+
+ @Test
+ public void testContains_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(LongStream.of(42)).contains(100));
+ }
+
+ @Test
+ public void testContainsAnyOf() throws Exception {
+ assertThat(LongStream.of(42)).containsAnyOf(42, 43);
+ }
+
+ @Test
+ public void testContainsAnyOf_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(LongStream.of(42)).containsAnyOf(43, 44));
+ }
+
+ @Test
+ public void testContainsAnyIn() throws Exception {
+ assertThat(LongStream.of(42)).containsAnyIn(asList(42L, 43L));
+ }
+
+ @Test
+ public void testContainsAnyIn_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(
+ whenTesting -> whenTesting.that(LongStream.of(42)).containsAnyIn(asList(43, 44)));
+ }
+
+ @Test
+ public void testDoesNotContain() throws Exception {
+ assertThat(LongStream.of(42)).doesNotContain(43);
+ }
+
+ @Test
+ public void testDoesNotContain_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(LongStream.of(42)).doesNotContain(42));
+ }
+
+ @Test
+ public void testContainsNoneOf() throws Exception {
+ assertThat(LongStream.of(42)).containsNoneOf(43, 44);
+ }
+
+ @Test
+ public void testContainsNoneOf_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(LongStream.of(42)).containsNoneOf(42, 43));
+ }
+
+ @Test
+ public void testContainsNoneIn() throws Exception {
+ assertThat(LongStream.of(42)).containsNoneIn(asList(43, 44));
+ }
+
+ @Test
+ public void testContainsNoneIn_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(
+ whenTesting -> whenTesting.that(LongStream.of(42)).containsNoneIn(asList(42L, 43L)));
+ }
+
+ @Test
+ public void testContainsAtLeast() throws Exception {
+ assertThat(LongStream.of(42, 43)).containsAtLeast(42, 43);
+ }
+
+ @Test
+ public void testContainsAtLeast_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(
+ whenTesting -> whenTesting.that(LongStream.of(42, 43)).containsAtLeast(42, 43, 44));
+ }
+
+ @Test
+ public void testContainsAtLeast_inOrder() throws Exception {
+ assertThat(LongStream.of(42, 43)).containsAtLeast(42, 43).inOrder();
+ }
+
+ @Test
+ public void testContainsAtLeast_inOrder_fails() throws Exception {
+ try {
+ assertThat(LongStream.of(42, 43)).containsAtLeast(43, 42).inOrder();
+ fail();
+ } catch (AssertionError expected) {
+ assertFailureKeys(
+ expected,
+ "required elements were all found, but order was wrong",
+ "expected order for required elements",
+ "but was");
+ assertFailureValue(expected, "expected order for required elements", "[43, 42]");
+ }
+ }
+
+ @Test
+ public void testContainsAtLeastElementsIn() throws Exception {
+ assertThat(LongStream.of(42, 43)).containsAtLeastElementsIn(asList(42L, 43L));
+ }
+
+ @Test
+ public void testContainsAtLeastElementsIn_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(
+ whenTesting ->
+ whenTesting
+ .that(LongStream.of(42, 43))
+ .containsAtLeastElementsIn(asList(42L, 43L, 44L)));
+ }
+
+ @Test
+ public void testContainsAtLeastElementsIn_wrongType_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(
+ whenTesting ->
+ whenTesting
+ .that(LongStream.of(42, 43))
+ .containsAtLeastElementsIn(asList(42, 43, 44)));
+ }
+
+ @Test
+ public void testContainsAtLeastElementsIn_inOrder() throws Exception {
+ assertThat(LongStream.of(42, 43)).containsAtLeastElementsIn(asList(42L, 43L)).inOrder();
+ }
+
+ @Test
+ public void testContainsAtLeastElementsIn_inOrder_fails() throws Exception {
+ try {
+ assertThat(LongStream.of(42, 43)).containsAtLeastElementsIn(asList(43L, 42L)).inOrder();
+ fail();
+ } catch (AssertionError expected) {
+ assertFailureKeys(
+ expected,
+ "required elements were all found, but order was wrong",
+ "expected order for required elements",
+ "but was");
+ assertFailureValue(expected, "expected order for required elements", "[43, 42]");
+ }
+ }
+
+ @Test
+ public void testContainsAtLeastElementsIn_inOrder_wrongType_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(
+ whenTesting ->
+ whenTesting
+ .that(LongStream.of(42, 43))
+ .containsAtLeastElementsIn(asList(43, 42))
+ .inOrder());
+ }
+
+ @Test
+ public void testContainsExactly() throws Exception {
+ assertThat(LongStream.of(42, 43)).containsExactly(42, 43);
+ }
+
+ @Test
+ public void testContainsExactly_fails() throws Exception {
+ try {
+ assertThat(LongStream.of(42, 43)).containsExactly(42);
+ fail();
+ } catch (AssertionError expected) {
+ assertFailureKeys(expected, "unexpected (1)", "---", "expected", "but was");
+ assertFailureValue(expected, "expected", "[42]");
+ }
+ }
+
+ @Test
+ public void testContainsExactly_inOrder() throws Exception {
+ assertThat(LongStream.of(42, 43)).containsExactly(42, 43).inOrder();
+ }
+
+ @Test
+ public void testContainsExactly_inOrder_fails() throws Exception {
+ try {
+ assertThat(LongStream.of(42, 43)).containsExactly(43, 42).inOrder();
+ fail();
+ } catch (AssertionError expected) {
+ assertFailureKeys(expected, "contents match, but order was wrong", "expected", "but was");
+ assertFailureValue(expected, "expected", "[43, 42]");
+ }
+ }
+
+ @Test
+ public void testContainsExactlyElementsIn() throws Exception {
+ assertThat(LongStream.of(42, 43)).containsExactlyElementsIn(asList(42L, 43L));
+ assertThat(LongStream.of(42, 43)).containsExactlyElementsIn(asList(43L, 42L));
+ }
+
+ @Test
+ public void testContainsExactlyElementsIn_fails() throws Exception {
+ try {
+ assertThat(LongStream.of(42, 43)).containsExactlyElementsIn(asList(42L));
+ fail();
+ } catch (AssertionError expected) {
+ assertFailureKeys(expected, "unexpected (1)", "---", "expected", "but was");
+ assertFailureValue(expected, "expected", "[42]");
+ }
+ }
+
+ @Test
+ public void testContainsExactlyElementsIn_wrongType_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(
+ whenTesting ->
+ whenTesting.that(LongStream.of(42, 43)).containsExactlyElementsIn(asList(42)));
+ }
+
+ @Test
+ public void testContainsExactlyElementsIn_inOrder() throws Exception {
+ assertThat(LongStream.of(42, 43)).containsExactlyElementsIn(asList(42L, 43L)).inOrder();
+ }
+
+ @Test
+ public void testContainsExactlyElementsIn_inOrder_fails() throws Exception {
+ try {
+ assertThat(LongStream.of(42, 43)).containsExactlyElementsIn(asList(43L, 42L)).inOrder();
+ fail();
+ } catch (AssertionError expected) {
+ assertFailureKeys(expected, "contents match, but order was wrong", "expected", "but was");
+ assertFailureValue(expected, "expected", "[43, 42]");
+ }
+ }
+
+ @Test
+ public void testContainsExactlyElementsIn_inOrder_wrongType_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(
+ whenTesting ->
+ whenTesting
+ .that(LongStream.of(42, 43))
+ .containsExactlyElementsIn(asList(43, 42))
+ .inOrder());
+ }
+
+ @Test
+ public void testContainsExactlyElementsIn_inOrder_LongStream() throws Exception {
+ assertThat(LongStream.of(1, 2, 3, 4)).containsExactly(1, 2, 3, 4).inOrder();
+ }
+
+ @Test
+ public void testIsInOrder() {
+ assertThat(LongStream.of()).isInOrder();
+ assertThat(LongStream.of(1)).isInOrder();
+ assertThat(LongStream.of(1, 1, 2, 3, 3, 3, 4)).isInOrder();
+ }
+
+ @Test
+ public void testIsInOrder_fails() {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(LongStream.of(1, 3, 2, 4)).isInOrder());
+ }
+
+ @Test
+ public void testIsInStrictOrder() {
+ assertThat(LongStream.of()).isInStrictOrder();
+ assertThat(LongStream.of(1)).isInStrictOrder();
+ assertThat(LongStream.of(1, 2, 3, 4)).isInStrictOrder();
+ }
+
+ @Test
+ public void testIsInStrictOrder_fails() {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(LongStream.of(1, 2, 2, 4)).isInStrictOrder());
+ }
+
+ private static AssertionError expectFailure(
+ ExpectFailure.SimpleSubjectBuilderCallback<LongStreamSubject, LongStream> assertionCallback) {
+ return ExpectFailure.expectFailureAbout(longStreams(), assertionCallback);
+ }
+}
diff --git a/extensions/java8/src/test/java/com/google/common/truth/OptionalDoubleSubjectTest.java b/extensions/java8/src/test/java/com/google/common/truth/OptionalDoubleSubjectTest.java
new file mode 100644
index 00000000..94490fa0
--- /dev/null
+++ b/extensions/java8/src/test/java/com/google/common/truth/OptionalDoubleSubjectTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.ExpectFailure.assertThat;
+import static com.google.common.truth.OptionalDoubleSubject.optionalDoubles;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
+
+import java.util.OptionalDouble;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Java 8 {@link OptionalDouble} Subjects.
+ *
+ * @author Ben Douglass
+ */
+@RunWith(JUnit4.class)
+public class OptionalDoubleSubjectTest {
+
+ @Test
+ public void failOnNullSubject() {
+ AssertionError expected = expectFailure(whenTesting -> whenTesting.that(null).isEmpty());
+ assertThat(expected).factKeys().containsExactly("expected empty optional", "but was").inOrder();
+ }
+
+ @Test
+ public void isPresent() {
+ assertThat(OptionalDouble.of(1337.0)).isPresent();
+ }
+
+ @Test
+ public void isPresentFailing() {
+ AssertionError expected =
+ expectFailure(whenTesting -> whenTesting.that(OptionalDouble.empty()).isPresent());
+ assertThat(expected).factKeys().containsExactly("expected to be present");
+ }
+
+ @Test
+ public void isEmpty() {
+ assertThat(OptionalDouble.empty()).isEmpty();
+ }
+
+ @Test
+ public void isEmptyFailing() {
+ AssertionError expected =
+ expectFailure(whenTesting -> whenTesting.that(OptionalDouble.of(1337.0)).isEmpty());
+ assertThat(expected).factKeys().contains("expected to be empty");
+ assertThat(expected).factValue("but was present with value").isEqualTo("1337.0");
+ }
+
+ @Test
+ public void isEmptyFailingNull() {
+ AssertionError expected = expectFailure(whenTesting -> whenTesting.that(null).isEmpty());
+ assertThat(expected).factKeys().containsExactly("expected empty optional", "but was").inOrder();
+ }
+
+ @Test
+ public void hasValue() {
+ assertThat(OptionalDouble.of(1337.0)).hasValue(1337.0);
+ }
+
+ @Test
+ public void hasValue_FailingWithEmpty() {
+ AssertionError expected =
+ expectFailure(whenTesting -> whenTesting.that(OptionalDouble.empty()).hasValue(1337.0));
+ assertThat(expected)
+ .factKeys()
+ .containsExactly("expected to have value", "but was absent")
+ .inOrder();
+ assertThat(expected).factValue("expected to have value").isEqualTo("1337.0");
+ }
+
+ @Test
+ public void hasValue_FailingWithWrongValue() {
+ AssertionError expected =
+ expectFailure(whenTesting -> whenTesting.that(OptionalDouble.of(1337.0)).hasValue(42.0));
+ assertThat(expected).factValue("value of").isEqualTo("optionalDouble.getAsDouble()");
+ }
+
+ private static AssertionError expectFailure(
+ ExpectFailure.SimpleSubjectBuilderCallback<OptionalDoubleSubject, OptionalDouble>
+ assertionCallback) {
+ return ExpectFailure.expectFailureAbout(optionalDoubles(), assertionCallback);
+ }
+}
diff --git a/extensions/java8/src/test/java/com/google/common/truth/OptionalIntSubjectTest.java b/extensions/java8/src/test/java/com/google/common/truth/OptionalIntSubjectTest.java
new file mode 100644
index 00000000..9c68a875
--- /dev/null
+++ b/extensions/java8/src/test/java/com/google/common/truth/OptionalIntSubjectTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.ExpectFailure.assertThat;
+import static com.google.common.truth.OptionalIntSubject.optionalInts;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
+
+import java.util.OptionalInt;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Java 8 {@link OptionalInt} Subjects.
+ *
+ * @author Ben Douglass
+ */
+@RunWith(JUnit4.class)
+public class OptionalIntSubjectTest {
+
+ @Test
+ public void failOnNullSubject() {
+ AssertionError expected = expectFailure(whenTesting -> whenTesting.that(null).isEmpty());
+ assertThat(expected).factKeys().containsExactly("expected empty optional", "but was").inOrder();
+ }
+
+ @Test
+ public void isPresent() {
+ assertThat(OptionalInt.of(1337)).isPresent();
+ }
+
+ @Test
+ public void isPresentFailing() {
+ AssertionError expected =
+ expectFailure(whenTesting -> whenTesting.that(OptionalInt.empty()).isPresent());
+ assertThat(expected).factKeys().containsExactly("expected to be present");
+ }
+
+ @Test
+ public void isEmpty() {
+ assertThat(OptionalInt.empty()).isEmpty();
+ }
+
+ @Test
+ public void isEmptyFailing() {
+ AssertionError expected =
+ expectFailure(whenTesting -> whenTesting.that(OptionalInt.of(1337)).isEmpty());
+ assertThat(expected).factKeys().contains("expected to be empty");
+ assertThat(expected).factValue("but was present with value").isEqualTo("1337");
+ }
+
+ @Test
+ public void isEmptyFailingNull() {
+ AssertionError expected = expectFailure(whenTesting -> whenTesting.that(null).isEmpty());
+ assertThat(expected).factKeys().containsExactly("expected empty optional", "but was").inOrder();
+ }
+
+ @Test
+ public void hasValue() {
+ assertThat(OptionalInt.of(1337)).hasValue(1337);
+ }
+
+ @Test
+ public void hasValue_FailingWithEmpty() {
+ AssertionError expected =
+ expectFailure(whenTesting -> whenTesting.that(OptionalInt.empty()).hasValue(1337));
+ assertThat(expected)
+ .factKeys()
+ .containsExactly("expected to have value", "but was absent")
+ .inOrder();
+ assertThat(expected).factValue("expected to have value").isEqualTo("1337");
+ }
+
+ @Test
+ public void hasValue_FailingWithWrongValue() {
+ AssertionError expected =
+ expectFailure(whenTesting -> whenTesting.that(OptionalInt.of(1337)).hasValue(42));
+ assertThat(expected).factValue("value of").isEqualTo("optionalInt.getAsInt()");
+ }
+
+ private static AssertionError expectFailure(
+ ExpectFailure.SimpleSubjectBuilderCallback<OptionalIntSubject, OptionalInt>
+ assertionCallback) {
+ return ExpectFailure.expectFailureAbout(optionalInts(), assertionCallback);
+ }
+}
diff --git a/extensions/java8/src/test/java/com/google/common/truth/OptionalLongSubjectTest.java b/extensions/java8/src/test/java/com/google/common/truth/OptionalLongSubjectTest.java
new file mode 100644
index 00000000..211ed504
--- /dev/null
+++ b/extensions/java8/src/test/java/com/google/common/truth/OptionalLongSubjectTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.ExpectFailure.assertThat;
+import static com.google.common.truth.OptionalLongSubject.optionalLongs;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
+
+import java.util.OptionalLong;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Java 8 {@link OptionalLong} Subjects.
+ *
+ * @author Ben Douglass
+ */
+@RunWith(JUnit4.class)
+public class OptionalLongSubjectTest {
+
+ @Test
+ public void failOnNullSubject() {
+ AssertionError expected = expectFailure(whenTesting -> whenTesting.that(null).isEmpty());
+ assertThat(expected).factKeys().containsExactly("expected empty optional", "but was").inOrder();
+ }
+
+ @Test
+ public void isPresent() {
+ assertThat(OptionalLong.of(1337L)).isPresent();
+ }
+
+ @Test
+ public void isPresentFailing() {
+ AssertionError expected =
+ expectFailure(whenTesting -> whenTesting.that(OptionalLong.empty()).isPresent());
+ assertThat(expected).factKeys().containsExactly("expected to be present");
+ }
+
+ @Test
+ public void isEmpty() {
+ assertThat(OptionalLong.empty()).isEmpty();
+ }
+
+ @Test
+ public void isEmptyFailing() {
+ AssertionError expected =
+ expectFailure(whenTesting -> whenTesting.that(OptionalLong.of(1337L)).isEmpty());
+ assertThat(expected).factKeys().contains("expected to be empty");
+ assertThat(expected).factValue("but was present with value").isEqualTo("1337");
+ }
+
+ @Test
+ public void isEmptyFailingNull() {
+ AssertionError expected = expectFailure(whenTesting -> whenTesting.that(null).isEmpty());
+ assertThat(expected).factKeys().containsExactly("expected empty optional", "but was").inOrder();
+ }
+
+ @Test
+ public void hasValue() {
+ assertThat(OptionalLong.of(1337L)).hasValue(1337L);
+ }
+
+ @Test
+ public void hasValue_FailingWithEmpty() {
+ AssertionError expected =
+ expectFailure(whenTesting -> whenTesting.that(OptionalLong.empty()).hasValue(1337L));
+ assertThat(expected)
+ .factKeys()
+ .containsExactly("expected to have value", "but was absent")
+ .inOrder();
+ assertThat(expected).factValue("expected to have value").isEqualTo("1337");
+ }
+
+ @Test
+ public void hasValue_FailingWithWrongValue() {
+ AssertionError expected =
+ expectFailure(whenTesting -> whenTesting.that(OptionalLong.of(1337L)).hasValue(42L));
+ assertThat(expected).factValue("value of").isEqualTo("optionalLong.getAsLong()");
+ }
+
+ private static AssertionError expectFailure(
+ ExpectFailure.SimpleSubjectBuilderCallback<OptionalLongSubject, OptionalLong>
+ assertionCallback) {
+ return ExpectFailure.expectFailureAbout(optionalLongs(), assertionCallback);
+ }
+}
diff --git a/extensions/java8/src/test/java/com/google/common/truth/OptionalSubjectTest.java b/extensions/java8/src/test/java/com/google/common/truth/OptionalSubjectTest.java
new file mode 100644
index 00000000..e1cdc566
--- /dev/null
+++ b/extensions/java8/src/test/java/com/google/common/truth/OptionalSubjectTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.ExpectFailure.assertThat;
+import static com.google.common.truth.OptionalSubject.optionals;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
+import static org.junit.Assert.fail;
+
+import java.util.Optional;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Java 8 {@link Optional} Subject.
+ *
+ * @author Christian Gruber (cgruber@israfil.net)
+ */
+@RunWith(JUnit4.class)
+public class OptionalSubjectTest {
+
+ @Test
+ public void isPresent() {
+ assertThat(Optional.of("foo")).isPresent();
+ }
+
+ @Test
+ public void isPresentFailing() {
+ AssertionError expected =
+ expectFailure(whenTesting -> whenTesting.that(Optional.empty()).isPresent());
+ assertThat(expected).factKeys().containsExactly("expected to be present");
+ }
+
+ @Test
+ public void isPresentFailingNull() {
+ AssertionError expected = expectFailure(whenTesting -> whenTesting.that(null).isPresent());
+ assertThat(expected)
+ .factKeys()
+ .containsExactly("expected present optional", "but was")
+ .inOrder();
+ }
+
+ @Test
+ public void isEmpty() {
+ assertThat(Optional.empty()).isEmpty();
+ }
+
+ @Test
+ public void isEmptyFailing() {
+ AssertionError expected =
+ expectFailure(whenTesting -> whenTesting.that(Optional.of("foo")).isEmpty());
+ assertThat(expected).factKeys().contains("expected to be empty");
+ assertThat(expected).factValue("but was present with value").isEqualTo("foo");
+ }
+
+ @Test
+ public void isEmptyFailingNull() {
+ AssertionError expected = expectFailure(whenTesting -> whenTesting.that(null).isEmpty());
+ assertThat(expected).factKeys().containsExactly("expected empty optional", "but was").inOrder();
+ }
+
+ @Test
+ public void hasValue() {
+ assertThat(Optional.of("foo")).hasValue("foo");
+ }
+
+ @Test
+ public void hasValue_failingWithEmpty() {
+ AssertionError expected =
+ expectFailure(whenTesting -> whenTesting.that(Optional.empty()).hasValue("foo"));
+ assertThat(expected)
+ .factKeys()
+ .containsExactly("expected to have value", "but was empty")
+ .inOrder();
+ assertThat(expected).factValue("expected to have value").isEqualTo("foo");
+ }
+
+ @Test
+ public void hasValue_npeWithNullParameter() {
+ try {
+ assertThat(Optional.of("foo")).hasValue(null);
+ fail("Expected NPE");
+ } catch (NullPointerException expected) {
+ assertThat(expected).hasMessageThat().isEqualTo("Optional cannot have a null value.");
+ }
+ }
+
+ @Test
+ public void hasValue_failingWithWrongValue() {
+ AssertionError expected =
+ expectFailure(whenTesting -> whenTesting.that(Optional.of("foo")).hasValue("boo"));
+ assertThat(expected).factValue("value of").isEqualTo("optional.get()");
+ }
+
+ private static AssertionError expectFailure(
+ ExpectFailure.SimpleSubjectBuilderCallback<OptionalSubject, Optional<?>> assertionCallback) {
+ return ExpectFailure.expectFailureAbout(optionals(), assertionCallback);
+ }
+}
diff --git a/extensions/java8/src/test/java/com/google/common/truth/PathSubjectTest.java b/extensions/java8/src/test/java/com/google/common/truth/PathSubjectTest.java
new file mode 100644
index 00000000..1e97f4d0
--- /dev/null
+++ b/extensions/java8/src/test/java/com/google/common/truth/PathSubjectTest.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2017 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.Truth8.assertThat;
+
+import java.nio.file.Paths;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class PathSubjectTest {
+ @Test
+ public void basicEquality() {
+ assertThat(Paths.get("foo")).isEqualTo(Paths.get("foo"));
+ }
+}
diff --git a/extensions/java8/src/test/java/com/google/common/truth/StreamSubjectTest.java b/extensions/java8/src/test/java/com/google/common/truth/StreamSubjectTest.java
new file mode 100644
index 00000000..b685e71b
--- /dev/null
+++ b/extensions/java8/src/test/java/com/google/common/truth/StreamSubjectTest.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth;
+
+import static com.google.common.truth.FailureAssertions.assertFailureKeys;
+import static com.google.common.truth.FailureAssertions.assertFailureValue;
+import static com.google.common.truth.StreamSubject.streams;
+import static com.google.common.truth.Truth8.assertThat;
+import static java.util.Arrays.asList;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+import java.util.stream.Stream;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Java 8 {@link Stream} Subjects.
+ *
+ * @author Kurt Alfred Kluever
+ */
+@RunWith(JUnit4.class)
+public final class StreamSubjectTest {
+
+ @Test
+ public void testIsEqualTo() throws Exception {
+ Stream<String> stream = Stream.of("hello");
+ assertThat(stream).isEqualTo(stream);
+ }
+
+ @Test
+ public void testIsEqualToList() throws Exception {
+ Stream<String> stream = Stream.of("hello");
+ List<String> list = asList("hello");
+ AssertionError unused = expectFailure(whenTesting -> whenTesting.that(stream).isEqualTo(list));
+ }
+
+ @Test
+ public void testNullStream_fails() throws Exception {
+ Stream<String> nullStream = null;
+ try {
+ assertThat(nullStream).isEmpty();
+ fail();
+ } catch (NullPointerException expected) {
+ }
+ }
+
+ @Test
+ public void testNullStreamIsNull() throws Exception {
+ Stream<String> nullStream = null;
+ assertThat(nullStream).isNull();
+ }
+
+ @Test
+ public void testIsSameInstanceAs() throws Exception {
+ Stream<String> stream = Stream.of("hello");
+ assertThat(stream).isSameInstanceAs(stream);
+ }
+
+ @Test
+ public void testIsEmpty() throws Exception {
+ assertThat(Stream.of()).isEmpty();
+ }
+
+ @Test
+ public void testIsEmpty_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(Stream.of("hello")).isEmpty());
+ }
+
+ @Test
+ public void testIsNotEmpty() throws Exception {
+ assertThat(Stream.of("hello")).isNotEmpty();
+ }
+
+ @Test
+ public void testIsNotEmpty_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(Stream.of()).isNotEmpty());
+ }
+
+ @Test
+ public void testHasSize() throws Exception {
+ assertThat(Stream.of("hello")).hasSize(1);
+ }
+
+ @Test
+ public void testHasSize_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(Stream.of("hello")).hasSize(2));
+ }
+
+ @Test
+ public void testContainsNoDuplicates() throws Exception {
+ assertThat(Stream.of("hello")).containsNoDuplicates();
+ }
+
+ @Test
+ public void testContainsNoDuplicates_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(
+ whenTesting -> whenTesting.that(Stream.of("hello", "hello")).containsNoDuplicates());
+ }
+
+ @Test
+ public void testContains() throws Exception {
+ assertThat(Stream.of("hello")).contains("hello");
+ }
+
+ @Test
+ public void testContains_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(Stream.of("hello")).contains("goodbye"));
+ }
+
+ @Test
+ public void testContainsAnyOf() throws Exception {
+ assertThat(Stream.of("hello")).containsAnyOf("hello", "hell");
+ }
+
+ @Test
+ public void testContainsAnyOf_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(
+ whenTesting -> whenTesting.that(Stream.of("hello")).containsAnyOf("goodbye", "good"));
+ }
+
+ @Test
+ public void testContainsAnyIn() throws Exception {
+ assertThat(Stream.of("hello")).containsAnyIn(asList("hello", "hell"));
+ }
+
+ @Test
+ public void testContainsAnyIn_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(
+ whenTesting ->
+ whenTesting.that(Stream.of("hello")).containsAnyIn(asList("goodbye", "good")));
+ }
+
+ @Test
+ public void testDoesNotContain() throws Exception {
+ assertThat(Stream.of("hello")).doesNotContain("goodbye");
+ }
+
+ @Test
+ public void testDoesNotContain_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(Stream.of("hello")).doesNotContain("hello"));
+ }
+
+ @Test
+ public void testContainsNoneOf() throws Exception {
+ assertThat(Stream.of("hello")).containsNoneOf("goodbye", "good");
+ }
+
+ @Test
+ public void testContainsNoneOf_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(
+ whenTesting -> whenTesting.that(Stream.of("hello")).containsNoneOf("hello", "hell"));
+ }
+
+ @Test
+ public void testContainsNoneIn() throws Exception {
+ assertThat(Stream.of("hello")).containsNoneIn(asList("goodbye", "good"));
+ }
+
+ @Test
+ public void testContainsNoneIn_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(
+ whenTesting ->
+ whenTesting.that(Stream.of("hello")).containsNoneIn(asList("hello", "hell")));
+ }
+
+ @Test
+ public void testContainsAtLeast() throws Exception {
+ assertThat(Stream.of("hell", "hello")).containsAtLeast("hell", "hello");
+ }
+
+ @Test
+ public void testContainsAtLeast_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(
+ whenTesting ->
+ whenTesting
+ .that(Stream.of("hell", "hello"))
+ .containsAtLeast("hell", "hello", "goodbye"));
+ }
+
+ @Test
+ public void testContainsAtLeast_inOrder() throws Exception {
+ assertThat(Stream.of("hell", "hello")).containsAtLeast("hell", "hello").inOrder();
+ }
+
+ @Test
+ public void testContainsAtLeast_inOrder_fails() throws Exception {
+ try {
+ assertThat(Stream.of("hell", "hello")).containsAtLeast("hello", "hell").inOrder();
+ fail();
+ } catch (AssertionError expected) {
+ assertFailureKeys(
+ expected,
+ "required elements were all found, but order was wrong",
+ "expected order for required elements",
+ "but was");
+ assertFailureValue(expected, "expected order for required elements", "[hello, hell]");
+ }
+ }
+
+ @Test
+ public void testContainsAtLeastElementsIn() throws Exception {
+ assertThat(Stream.of("hell", "hello")).containsAtLeastElementsIn(asList("hell", "hello"));
+ }
+
+ @Test
+ public void testContainsAtLeastElementsIn_fails() throws Exception {
+ AssertionError unused =
+ expectFailure(
+ whenTesting ->
+ whenTesting
+ .that(Stream.of("hell", "hello"))
+ .containsAtLeastElementsIn(asList("hell", "hello", "goodbye")));
+ }
+
+ @Test
+ public void testContainsAtLeastElementsIn_inOrder() throws Exception {
+ assertThat(Stream.of("hell", "hello"))
+ .containsAtLeastElementsIn(asList("hell", "hello"))
+ .inOrder();
+ }
+
+ @Test
+ public void testContainsAtLeastElementsIn_inOrder_fails() throws Exception {
+ try {
+ assertThat(Stream.of("hell", "hello"))
+ .containsAtLeastElementsIn(asList("hello", "hell"))
+ .inOrder();
+ fail();
+ } catch (AssertionError expected) {
+ assertFailureKeys(
+ expected,
+ "required elements were all found, but order was wrong",
+ "expected order for required elements",
+ "but was");
+ assertFailureValue(expected, "expected order for required elements", "[hello, hell]");
+ }
+ }
+
+ @Test
+ public void testContainsExactly() throws Exception {
+ assertThat(Stream.of("hell", "hello")).containsExactly("hell", "hello");
+ assertThat(Stream.of("hell", "hello")).containsExactly("hello", "hell");
+ }
+
+ @Test
+ public void testContainsExactly_fails() throws Exception {
+ try {
+ assertThat(Stream.of("hell", "hello")).containsExactly("hell");
+ fail();
+ } catch (AssertionError expected) {
+ assertFailureKeys(expected, "unexpected (1)", "---", "expected", "but was");
+ assertFailureValue(expected, "expected", "[hell]");
+ }
+ }
+
+ @Test
+ public void testContainsExactly_inOrder() throws Exception {
+ assertThat(Stream.of("hell", "hello")).containsExactly("hell", "hello").inOrder();
+ }
+
+ @Test
+ public void testContainsExactly_inOrder_fails() throws Exception {
+ try {
+ assertThat(Stream.of("hell", "hello")).containsExactly("hello", "hell").inOrder();
+ fail();
+ } catch (AssertionError expected) {
+ assertFailureKeys(expected, "contents match, but order was wrong", "expected", "but was");
+ assertFailureValue(expected, "expected", "[hello, hell]");
+ }
+ }
+
+ @Test
+ public void testContainsExactlyElementsIn() throws Exception {
+ assertThat(Stream.of("hell", "hello")).containsExactlyElementsIn(asList("hell", "hello"));
+ assertThat(Stream.of("hell", "hello")).containsExactlyElementsIn(asList("hello", "hell"));
+ }
+
+ @Test
+ public void testContainsExactlyElementsIn_fails() throws Exception {
+ try {
+ assertThat(Stream.of("hell", "hello")).containsExactlyElementsIn(asList("hell"));
+ fail();
+ } catch (AssertionError expected) {
+ assertFailureKeys(expected, "unexpected (1)", "---", "expected", "but was");
+ assertFailureValue(expected, "expected", "[hell]");
+ }
+ }
+
+ @Test
+ public void testContainsExactlyElementsIn_inOrder() throws Exception {
+ assertThat(Stream.of("hell", "hello"))
+ .containsExactlyElementsIn(asList("hell", "hello"))
+ .inOrder();
+ }
+
+ @Test
+ public void testContainsExactlyElementsIn_inOrder_fails() throws Exception {
+ try {
+ assertThat(Stream.of("hell", "hello"))
+ .containsExactlyElementsIn(asList("hello", "hell"))
+ .inOrder();
+ fail();
+ } catch (AssertionError expected) {
+ assertFailureKeys(expected, "contents match, but order was wrong", "expected", "but was");
+ assertFailureValue(expected, "expected", "[hello, hell]");
+ }
+ }
+
+ @Test
+ public void testIsInOrder() {
+ assertThat(Stream.of()).isInOrder();
+ assertThat(Stream.of(1)).isInOrder();
+ assertThat(Stream.of(1, 1, 2, 3, 3, 3, 4)).isInOrder();
+ }
+
+ @Test
+ public void testIsInOrder_fails() {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(Stream.of(1, 3, 2, 4)).isInOrder());
+ }
+
+ @Test
+ public void testIsInStrictOrder() {
+ assertThat(Stream.of()).isInStrictOrder();
+ assertThat(Stream.of(1)).isInStrictOrder();
+ assertThat(Stream.of(1, 2, 3, 4)).isInStrictOrder();
+ }
+
+ @Test
+ public void testIsInStrictOrder_fails() {
+ AssertionError unused =
+ expectFailure(whenTesting -> whenTesting.that(Stream.of(1, 2, 2, 4)).isInStrictOrder());
+ }
+
+ private static AssertionError expectFailure(
+ ExpectFailure.SimpleSubjectBuilderCallback<StreamSubject, Stream<?>> assertionCallback) {
+ return ExpectFailure.expectFailureAbout(streams(), assertionCallback);
+ }
+}
diff --git a/extensions/liteproto/pom.xml b/extensions/liteproto/pom.xml
new file mode 100644
index 00000000..82bdc46d
--- /dev/null
+++ b/extensions/liteproto/pom.xml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>com.google.truth.extensions</groupId>
+ <artifactId>truth-extensions-parent</artifactId>
+ <version>HEAD-SNAPSHOT</version>
+ </parent>
+ <artifactId>truth-liteproto-extension</artifactId>
+ <name>Truth Extension for Lite Protocol Buffers</name>
+ <description>
+ An extension for the Truth test assertion framework supporting the Lite
+ version of Protocol Buffers.
+ </description>
+ <dependencies>
+ <dependency>
+ <groupId>com.google.truth</groupId>
+ <artifactId>truth</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.checkerframework</groupId>
+ <artifactId>checker-qual</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.auto.value</groupId>
+ <artifactId>auto-value-annotations</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.errorprone</groupId>
+ <artifactId>error_prone_annotations</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.protobuf</groupId>
+ <artifactId>protobuf-lite</artifactId>
+ <!-- protobuf-lite is not compatible with protobuf-java, so we mark it optional. -->
+ <optional>true</optional>
+ </dependency>
+ </dependencies>
+ <build>
+ <extensions>
+ <extension>
+ <groupId>kr.motd.maven</groupId>
+ <artifactId>os-maven-plugin</artifactId>
+ <version>${os-maven-plugin.version}</version>
+ </extension>
+ </extensions>
+
+ <plugins>
+ <plugin>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.xolstice.maven.plugins</groupId>
+ <artifactId>protobuf-maven-plugin</artifactId>
+ <configuration>
+ <protocArtifact>com.google.protobuf:protoc:${protobuf-lite.protoc.version}:exe:${os.detected.classifier}</protocArtifact>
+ <includes>
+ <param>test_message_lite2.proto</param>
+ <param>test_message_lite3.proto</param>
+ </includes>
+ </configuration>
+ <executions>
+ <execution>
+ <id>build-test-protos</id>
+ <phase>generate-test-sources</phase>
+ <goals>
+ <goal>test-compile</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <annotationProcessorPaths>
+ <path>
+ <groupId>com.google.auto.value</groupId>
+ <artifactId>auto-value</artifactId>
+ <version>${auto-value.version}</version>
+ </path>
+ </annotationProcessorPaths>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
+
+
diff --git a/extensions/liteproto/src/main/java/com/google/common/truth/extensions/proto/LiteProtoSubject.java b/extensions/liteproto/src/main/java/com/google/common/truth/extensions/proto/LiteProtoSubject.java
new file mode 100644
index 00000000..8bee5946
--- /dev/null
+++ b/extensions/liteproto/src/main/java/com/google/common/truth/extensions/proto/LiteProtoSubject.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import static com.google.common.base.Strings.lenientFormat;
+import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.Fact.simpleFact;
+
+import com.google.common.base.Objects;
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IntegerSubject;
+import com.google.common.truth.Subject;
+import com.google.errorprone.annotations.CheckReturnValue;
+import com.google.protobuf.MessageLite;
+import java.util.regex.Pattern;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Truth subjects for the Lite version of Protocol Buffers.
+ *
+ * <p>LiteProtoSubject supports versions 2 and 3 of Protocol Buffers. Due to the lack of runtime
+ * descriptors, its functionality is limited compared to ProtoSubject, in particular in performing
+ * detailed comparisons between messages.
+ */
+@CheckReturnValue
+public class LiteProtoSubject extends Subject {
+
+ /**
+ * Returns a {@code Subject.Factory} for {@link MessageLite} subjects which you can use to assert
+ * things about Lite Protobuf properties.
+ */
+ static Factory<LiteProtoSubject, MessageLite> liteProtos() {
+ return LiteProtoSubjectFactory.INSTANCE;
+ }
+
+ /*
+ * Storing a FailureMetadata instance in a Subject subclass is generally a bad practice: The
+ * FailureMetadata instance contains the chain of subjects used to create this one (as in
+ * assertThat(value).hasFooThat().hasBarThat()). Reusing that instance for a different Subject
+ * (like one produced by a hasBazThat() method) would result in a Subject with the wrong chain,
+ * potentially producing misleading failure messages. To get the messages right, Subject
+ * subclasses should instead use check(...).
+ *
+ * However... here we are using the FailureMetadata instance only to create a Subject for an
+ * "equivalent" object. Thus, it will still be accurate for the failure message to be written as
+ * if it's talking about the proto itself.
+ *
+ * TODO(b/127819891): Use a better API for this if one is addded.
+ */
+ private final FailureMetadata metadata;
+ private final MessageLite actual;
+
+ protected LiteProtoSubject(FailureMetadata failureMetadata, @Nullable MessageLite messageLite) {
+ super(failureMetadata, messageLite);
+ this.metadata = failureMetadata;
+ this.actual = messageLite;
+ }
+
+ // It is wrong to compare protos using their string representations. The MessageLite runtime
+ // deliberately prefixes debug strings with their Object.toString() to discourage string
+ // comparison. However, this reads poorly in tests, and makes it harder to identify differences
+ // from the strings alone. So, we manually strip this prefix.
+ // In case the class names are actually relevant, Subject.isEqualTo() will add them back for us.
+ // TODO(user): Maybe get a way to do this upstream.
+ static String getTrimmedToString(@Nullable MessageLite messageLite) {
+ String subjectString = String.valueOf(messageLite);
+ String trimmedSubjectString = subjectString.trim();
+ if (trimmedSubjectString.startsWith("# ")) {
+ String objectToString =
+ String.format(
+ "# %s@%s",
+ messageLite.getClass().getName(), Integer.toHexString(messageLite.hashCode()));
+ if (trimmedSubjectString.startsWith(objectToString)) {
+ subjectString = trimmedSubjectString.replaceFirst(Pattern.quote(objectToString), "").trim();
+ }
+ }
+
+ return subjectString.isEmpty() ? "[empty proto]" : subjectString;
+ }
+
+ @Override
+ protected String actualCustomStringRepresentation() {
+ return actualCustomStringRepresentationForProtoPackageMembersToCall();
+ }
+
+ final String actualCustomStringRepresentationForProtoPackageMembersToCall() {
+ return getTrimmedToString(actual);
+ }
+
+ /**
+ * Checks whether the MessageLite is equivalent to the argument, using the standard equals()
+ * implementation.
+ */
+ @Override
+ public void isEqualTo(@Nullable Object expected) {
+ // TODO(user): Do better here when MessageLite descriptors are available.
+ if (Objects.equal(actual, expected)) {
+ return;
+ }
+
+ if (actual == null || expected == null) {
+ super.isEqualTo(expected);
+ } else if (actual.getClass() != expected.getClass()) {
+ failWithoutActual(
+ simpleFact(
+ lenientFormat(
+ "Not true that (%s) proto is equal to the expected (%s) object. "
+ + "They are not of the same class.",
+ actual.getClass().getName(), expected.getClass().getName())));
+ } else {
+ /*
+ * TODO(cpovirk): If we someday let subjects override formatActualOrExpected(), change this
+ * class to do so, and make this code path always delegate to super.isEqualTo().
+ */
+ String ourString = getTrimmedToString(actual);
+ String theirString = getTrimmedToString((MessageLite) expected);
+ if (!ourString.equals(theirString)) {
+ new LiteProtoAsStringSubject(metadata, ourString).isEqualTo(theirString); // fails
+ } else {
+ // This will include the Object.toString() headers.
+ super.isEqualTo(expected);
+ }
+ }
+ }
+
+ /**
+ * @deprecated A Builder can never compare equal to a MessageLite instance. Use {@code build()},
+ * or {@code buildPartial()} on the argument to get a MessageLite for comparison instead.
+ */
+ @Deprecated
+ public void isEqualTo(MessageLite./*@Nullable*/ Builder builder) {
+ isEqualTo((Object) builder);
+ }
+
+ private static final class LiteProtoAsStringSubject extends Subject {
+ LiteProtoAsStringSubject(FailureMetadata metadata, @Nullable String actual) {
+ super(metadata, actual);
+ }
+ }
+
+ @Override
+ public void isNotEqualTo(@Nullable Object expected) {
+ if (Objects.equal(actual, expected)) {
+ if (actual == null) {
+ super.isNotEqualTo(expected);
+ } else {
+ failWithoutActual(
+ simpleFact(
+ lenientFormat(
+ "Not true that protos are different. Both are (%s) <%s>.",
+ actual.getClass().getName(), getTrimmedToString(actual))));
+ }
+ }
+ }
+
+ /**
+ * @deprecated A Builder will never compare equal to a MessageLite instance. Use {@code build()},
+ * or {@code buildPartial()} on the argument to get a MessageLite for comparison instead.
+ */
+ @Deprecated
+ public void isNotEqualTo(MessageLite./*@Nullable*/ Builder builder) {
+ isNotEqualTo((Object) builder);
+ }
+
+ /** Checks whether the subject is a {@link MessageLite} with no fields set. */
+ public void isEqualToDefaultInstance() {
+ if (actual == null) {
+ failWithoutActual(
+ simpleFact(
+ lenientFormat(
+ "Not true that <%s> is a default proto instance. It is null.",
+ actualCustomStringRepresentationForProtoPackageMembersToCall())));
+ } else if (!actual.equals(actual.getDefaultInstanceForType())) {
+ failWithoutActual(
+ simpleFact(
+ lenientFormat(
+ "Not true that <%s> is a default proto instance. It has set values.",
+ actualCustomStringRepresentationForProtoPackageMembersToCall())));
+ }
+ }
+
+ /** Checks whether the subject is not equivalent to a {@link MessageLite} with no fields set. */
+ public void isNotEqualToDefaultInstance() {
+ if (actual != null && actual.equals(actual.getDefaultInstanceForType())) {
+ failWithoutActual(
+ simpleFact(
+ lenientFormat(
+ "Not true that (%s) <%s> is not a default proto instance. It has no set values.",
+ actual.getClass().getName(),
+ actualCustomStringRepresentationForProtoPackageMembersToCall())));
+ }
+ }
+
+ /**
+ * Checks whether the subject has all required fields set. Cannot fail for a proto built with
+ * {@code build()}, which itself fails if required fields aren't set.
+ */
+ public void hasAllRequiredFields() {
+ if (!actual.isInitialized()) {
+ // MessageLite doesn't support reflection so this is the best we can do.
+ failWithoutActual(
+ simpleFact("expected to have all required fields set"),
+ fact("but was", actualCustomStringRepresentationForProtoPackageMembersToCall()),
+ simpleFact("(Lite runtime could not determine which fields were missing.)"));
+ }
+ }
+
+ /**
+ * Returns an {@link IntegerSubject} on the serialized size of the MessageLite.
+ *
+ * <p>Assertions can then be changed on the serialized size, to support checks such as {@code
+ * assertThat(myProto).serializedSize().isAtLeast(16)}, etc.
+ */
+ public IntegerSubject serializedSize() {
+ return check("getSerializedSize()").that(actual.getSerializedSize());
+ }
+
+ private static final class LiteProtoSubjectFactory
+ implements Factory<LiteProtoSubject, MessageLite> {
+ private static final LiteProtoSubjectFactory INSTANCE = new LiteProtoSubjectFactory();
+
+ @Override
+ public LiteProtoSubject createSubject(
+ FailureMetadata failureMetadata, @Nullable MessageLite messageLite) {
+ return new LiteProtoSubject(failureMetadata, messageLite);
+ }
+ }
+}
diff --git a/extensions/liteproto/src/main/java/com/google/common/truth/extensions/proto/LiteProtoTruth.java b/extensions/liteproto/src/main/java/com/google/common/truth/extensions/proto/LiteProtoTruth.java
new file mode 100644
index 00000000..94895d93
--- /dev/null
+++ b/extensions/liteproto/src/main/java/com/google/common/truth/extensions/proto/LiteProtoTruth.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import static com.google.common.truth.Truth.assertAbout;
+
+import com.google.common.truth.Subject;
+import com.google.errorprone.annotations.CheckReturnValue;
+import com.google.protobuf.MessageLite;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A set of static methods to begin a Truth assertion chain for the lite version of protocol
+ * buffers.
+ *
+ * <p>This class implements a subset of what {@code ProtoTruth} provides, so if you are already
+ * using {@code ProtoTruth}, you should not import this class. {@code LiteProtoTruth} is only useful
+ * if you cannot depend on {@code ProtoTruth} for dependency management reasons.
+ *
+ * <p>Note: Usage of different failure strategies such as <em>assume</em> and <em>expect</em> should
+ * rely on {@linkplain com.google.common.truth.StandardSubjectBuilder#about(Subject.Factory)
+ * about(liteProtos())} to begin a chain with those alternative behaviors.
+ */
+@CheckReturnValue
+public final class LiteProtoTruth {
+ public static LiteProtoSubject assertThat(@Nullable MessageLite messageLite) {
+ return assertAbout(liteProtos()).that(messageLite);
+ }
+
+ public static Subject.Factory<LiteProtoSubject, MessageLite> liteProtos() {
+ return LiteProtoSubject.liteProtos();
+ }
+
+ private LiteProtoTruth() {}
+}
diff --git a/extensions/liteproto/src/test/java/com/google/common/truth/extensions/proto/LiteProtoSubjectTest.java b/extensions/liteproto/src/test/java/com/google/common/truth/extensions/proto/LiteProtoSubjectTest.java
new file mode 100644
index 00000000..c3e0ed2d
--- /dev/null
+++ b/extensions/liteproto/src/test/java/com/google/common/truth/extensions/proto/LiteProtoSubjectTest.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import static com.google.common.truth.ExpectFailure.assertThat;
+import static com.google.common.truth.extensions.proto.LiteProtoTruth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.truth.Expect;
+import com.google.common.truth.Subject;
+import com.google.protobuf.MessageLite;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.regex.Pattern;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/** Unit tests for {@link LiteProtoSubject}. */
+@RunWith(Parameterized.class)
+public class LiteProtoSubjectTest {
+
+ /**
+ * We run (almost) all the tests for both proto2 and proto3 implementations. This class organizes
+ * the parameters much more cleanly than a raw Object[].
+ */
+ @AutoValue
+ public abstract static class Config {
+ abstract MessageLite nonEmptyMessage();
+
+ abstract MessageLite equivalentNonEmptyMessage();
+
+ abstract MessageLite nonEmptyMessageOfOtherValue();
+
+ abstract MessageLite nonEmptyMessageOfOtherType();
+
+ abstract MessageLite defaultInstance();
+
+ abstract MessageLite defaultInstanceOfOtherType();
+
+ abstract Optional<MessageLite> messageWithoutRequiredFields();
+
+ public static Builder newBuilder() {
+ return new AutoValue_LiteProtoSubjectTest_Config.Builder();
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ abstract Builder setNonEmptyMessage(MessageLite messageLite);
+
+ abstract Builder setEquivalentNonEmptyMessage(MessageLite messageLite);
+
+ abstract Builder setNonEmptyMessageOfOtherValue(MessageLite messageLite);
+
+ abstract Builder setNonEmptyMessageOfOtherType(MessageLite messageLite);
+
+ abstract Builder setDefaultInstance(MessageLite messageLite);
+
+ abstract Builder setDefaultInstanceOfOtherType(MessageLite messageLite);
+
+ abstract Builder setMessageWithoutRequiredFields(MessageLite messageLite);
+
+ abstract Config build();
+ }
+ }
+
+ @Parameters(name = "{0}")
+ public static Collection<Object[]> data() {
+ // Missing a required_int field.
+ TestMessageLite2WithRequiredFields withoutRequiredFields =
+ TestMessageLite2WithRequiredFields.newBuilder().setOptionalString("foo").buildPartial();
+
+ Config proto2Config =
+ Config.newBuilder()
+ .setNonEmptyMessage(TestMessageLite2.newBuilder().setOptionalInt(3).build())
+ .setEquivalentNonEmptyMessage(TestMessageLite2.newBuilder().setOptionalInt(3).build())
+ .setNonEmptyMessageOfOtherValue(
+ TestMessageLite2.newBuilder()
+ .setOptionalInt(3)
+ .setSubMessage(
+ TestMessageLite2.SubMessage.newBuilder().setOptionalString("foo"))
+ .build())
+ .setNonEmptyMessageOfOtherType(
+ OtherTestMessageLite2.newBuilder().setOptionalInt(3).build())
+ .setDefaultInstance(TestMessageLite2.newBuilder().buildPartial())
+ .setDefaultInstanceOfOtherType(OtherTestMessageLite2.newBuilder().buildPartial())
+ .setMessageWithoutRequiredFields(withoutRequiredFields)
+ .build();
+ Config proto3Config =
+ Config.newBuilder()
+ .setNonEmptyMessage(TestMessageLite3.newBuilder().setOptionalInt(3).build())
+ .setEquivalentNonEmptyMessage(TestMessageLite3.newBuilder().setOptionalInt(3).build())
+ .setNonEmptyMessageOfOtherValue(
+ TestMessageLite3.newBuilder()
+ .setOptionalInt(3)
+ .setSubMessage(
+ TestMessageLite3.SubMessage.newBuilder().setOptionalString("foo"))
+ .build())
+ .setNonEmptyMessageOfOtherType(
+ OtherTestMessageLite3.newBuilder().setOptionalInt(3).build())
+ .setDefaultInstance(TestMessageLite3.newBuilder().buildPartial())
+ .setDefaultInstanceOfOtherType(OtherTestMessageLite3.newBuilder().buildPartial())
+ .build();
+ return ImmutableList.of(
+ new Object[] {"Proto 2", proto2Config}, new Object[] {"Proto 3", proto3Config});
+ }
+
+ @Rule public final Expect expect = Expect.create();
+ private final Config config;
+
+ public LiteProtoSubjectTest(@SuppressWarnings("unused") String name, Config config) {
+ this.config = config;
+ }
+
+ private LiteProtoSubject expectThat(@Nullable MessageLite m) {
+ return expect.about(LiteProtoTruth.liteProtos()).that(m);
+ }
+
+ private Subject expectThat(@Nullable Object o) {
+ return expect.that(o);
+ }
+
+ @Test
+ public void testSubjectMethods() {
+ expectThat(config.nonEmptyMessage()).isSameInstanceAs(config.nonEmptyMessage());
+ expectThat(config.nonEmptyMessage().toBuilder()).isNotSameInstanceAs(config.nonEmptyMessage());
+
+ expectThat(config.nonEmptyMessage()).isInstanceOf(MessageLite.class);
+ expectThat(config.nonEmptyMessage().toBuilder()).isInstanceOf(MessageLite.Builder.class);
+ expectThat(config.nonEmptyMessage()).isNotInstanceOf(MessageLite.Builder.class);
+ expectThat(config.nonEmptyMessage().toBuilder()).isNotInstanceOf(MessageLite.class);
+
+ expectThat(config.nonEmptyMessage()).isIn(Arrays.asList(config.nonEmptyMessage()));
+ expectThat(config.nonEmptyMessage())
+ .isNotIn(Arrays.asList(config.nonEmptyMessageOfOtherValue()));
+ expectThat(config.nonEmptyMessage())
+ .isAnyOf(config.nonEmptyMessage(), config.nonEmptyMessageOfOtherValue());
+ expectThat(config.nonEmptyMessage())
+ .isNoneOf(config.nonEmptyMessageOfOtherValue(), config.nonEmptyMessageOfOtherType());
+ }
+
+ @Test
+ public void testIsEqualTo_success() {
+ expectThat(null).isEqualTo(null);
+ expectThat(null).isNull();
+
+ expectThat(config.nonEmptyMessage()).isEqualTo(config.nonEmptyMessage());
+ expectThat(config.nonEmptyMessage()).isEqualTo(config.equivalentNonEmptyMessage());
+ expectThat(config.nonEmptyMessage()).isNotEqualTo(config.nonEmptyMessage().toBuilder());
+
+ assertThat(config.defaultInstance()).isNotEqualTo(config.defaultInstanceOfOtherType());
+ assertThat(config.nonEmptyMessage()).isNotEqualTo(config.nonEmptyMessageOfOtherType());
+ assertThat(config.nonEmptyMessage()).isNotEqualTo(config.nonEmptyMessageOfOtherValue());
+ }
+
+ @Test
+ public void testIsEqualTo_failure() {
+ try {
+ assertThat(config.nonEmptyMessage()).isEqualTo(config.nonEmptyMessageOfOtherValue());
+ fail("Should have failed.");
+ } catch (AssertionError e) {
+ expectRegex(e, ".*expected:.*\"foo\".*");
+ expectNoRegex(e, ".*but was:.*\"foo\".*");
+ }
+
+ try {
+ assertThat(config.nonEmptyMessage()).isEqualTo(config.nonEmptyMessageOfOtherType());
+ fail("Should have failed.");
+ } catch (AssertionError e) {
+ expectRegex(
+ e,
+ "Not true that \\(.*\\) proto is equal to the expected \\(.*\\) object\\.\\s*"
+ + "They are not of the same class\\.");
+ }
+
+ try {
+ assertThat(config.nonEmptyMessage()).isNotEqualTo(config.equivalentNonEmptyMessage());
+ fail("Should have failed.");
+ } catch (AssertionError e) {
+ expectRegex(
+ e,
+ String.format(
+ "Not true that protos are different\\.\\s*Both are \\(%s\\) <.*optional_int: 3.*>\\.",
+ Pattern.quote(config.nonEmptyMessage().getClass().getName())));
+ }
+ }
+
+ @Test
+ public void testHasAllRequiredFields_success() {
+ expectThat(config.nonEmptyMessage()).hasAllRequiredFields();
+ }
+
+ @Test
+ public void testHasAllRequiredFields_failures() {
+ if (!config.messageWithoutRequiredFields().isPresent()) {
+ return;
+ }
+
+ try {
+ assertThat(config.messageWithoutRequiredFields().get()).hasAllRequiredFields();
+ fail("Should have failed.");
+ } catch (AssertionError e) {
+ expectRegex(
+ e,
+ "expected to have all required fields set\\s*but was: .*\\(Lite runtime could not"
+ + " determine which fields were missing.\\)");
+ }
+ }
+
+ @Test
+ public void testDefaultInstance_success() {
+ expectThat(config.defaultInstance()).isEqualToDefaultInstance();
+ expectThat(config.defaultInstanceOfOtherType()).isEqualToDefaultInstance();
+ expectThat(config.nonEmptyMessage().getDefaultInstanceForType()).isEqualToDefaultInstance();
+
+ expectThat(null).isNotEqualToDefaultInstance();
+ expectThat(config.nonEmptyMessage()).isNotEqualToDefaultInstance();
+ }
+
+ @Test
+ public void testDefaultInstance_failure() {
+ try {
+ assertThat(config.nonEmptyMessage()).isEqualToDefaultInstance();
+ fail("Should have failed.");
+ } catch (AssertionError e) {
+ expectRegex(
+ e,
+ "Not true that <.*optional_int:\\s*3.*> is a default proto instance\\.\\s*"
+ + "It has set values\\.");
+ }
+
+ try {
+ assertThat(config.defaultInstance()).isNotEqualToDefaultInstance();
+ fail("Should have failed.");
+ } catch (AssertionError e) {
+ expectRegex(
+ e,
+ String.format(
+ "Not true that \\(%s\\) <.*\\[empty proto\\].*> is not a default "
+ + "proto instance\\.\\s*It has no set values\\.",
+ Pattern.quote(config.defaultInstance().getClass().getName())));
+ }
+ }
+
+ @Test
+ public void testSerializedSize_success() {
+ int size = config.nonEmptyMessage().getSerializedSize();
+ expectThat(config.nonEmptyMessage()).serializedSize().isEqualTo(size);
+ expectThat(config.defaultInstance()).serializedSize().isEqualTo(0);
+ }
+
+ @Test
+ public void testSerializedSize_failure() {
+ int size = config.nonEmptyMessage().getSerializedSize();
+
+ try {
+ assertThat(config.nonEmptyMessage()).serializedSize().isGreaterThan(size);
+ fail("Should have failed.");
+ } catch (AssertionError e) {
+ assertThat(e).factValue("value of").isEqualTo("liteProto.getSerializedSize()");
+ assertThat(e).factValue("liteProto was").containsMatch("optional_int:\\s*3");
+ }
+
+ try {
+ assertThat(config.defaultInstance()).serializedSize().isGreaterThan(0);
+ fail("Should have failed.");
+ } catch (AssertionError e) {
+ assertThat(e).factValue("value of").isEqualTo("liteProto.getSerializedSize()");
+ assertThat(e).factValue("liteProto was").contains("[empty proto]");
+ }
+ }
+
+ private void expectRegex(AssertionError e, String regex) {
+ expect.that(e).hasMessageThat().matches(Pattern.compile(regex, Pattern.DOTALL));
+ }
+
+ private void expectNoRegex(AssertionError e, String regex) {
+ expect.that(e).hasMessageThat().doesNotMatch(Pattern.compile(regex, Pattern.DOTALL));
+ }
+}
diff --git a/extensions/liteproto/src/test/proto/test_message_lite2.proto b/extensions/liteproto/src/test/proto/test_message_lite2.proto
new file mode 100644
index 00000000..c2dc2593
--- /dev/null
+++ b/extensions/liteproto/src/test/proto/test_message_lite2.proto
@@ -0,0 +1,26 @@
+syntax = "proto2";
+
+package com.google.common.truth.extensions.proto;
+
+option optimize_for = LITE_RUNTIME;
+
+option java_package = "com.google.common.truth.extensions.proto";
+option java_multiple_files = true;
+
+message TestMessageLite2 {
+ optional int32 optional_int = 1;
+
+ message SubMessage {
+ optional string optional_string = 1;
+ }
+ optional SubMessage sub_message = 2;
+}
+
+message OtherTestMessageLite2 {
+ optional int32 optional_int = 1;
+}
+
+message TestMessageLite2WithRequiredFields {
+ required int32 required_int = 1;
+ optional string optional_string = 2;
+}
diff --git a/extensions/liteproto/src/test/proto/test_message_lite3.proto b/extensions/liteproto/src/test/proto/test_message_lite3.proto
new file mode 100644
index 00000000..ba3e3414
--- /dev/null
+++ b/extensions/liteproto/src/test/proto/test_message_lite3.proto
@@ -0,0 +1,21 @@
+syntax = "proto3";
+
+package com.google.common.truth.extensions.proto;
+
+option optimize_for = LITE_RUNTIME;
+
+option java_package = "com.google.common.truth.extensions.proto";
+option java_multiple_files = true;
+
+message TestMessageLite3 {
+ int32 optional_int = 1;
+
+ message SubMessage {
+ string optional_string = 1;
+ }
+ SubMessage sub_message = 2;
+}
+
+message OtherTestMessageLite3 {
+ int32 optional_int = 1;
+}
diff --git a/extensions/pom.xml b/extensions/pom.xml
new file mode 100644
index 00000000..a3af9292
--- /dev/null
+++ b/extensions/pom.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>com.google.truth</groupId>
+ <artifactId>truth-parent</artifactId>
+ <version>HEAD-SNAPSHOT</version>
+ </parent>
+ <groupId>com.google.truth.extensions</groupId>
+ <artifactId>truth-extensions-parent</artifactId>
+ <packaging>pom</packaging>
+ <name>Truth Extensions (Parent)</name>
+ <description>
+ Parent metdata for a collection of Truth extensions, Subjects, utilities for
+ the Truth assertion framework.
+ </description>
+ <modules>
+ <module>java8</module>
+ <module>re2j</module>
+ <module>liteproto</module>
+ <module>proto</module>
+ </modules>
+</project>
diff --git a/extensions/proto/pom.xml b/extensions/proto/pom.xml
new file mode 100644
index 00000000..b69b8e24
--- /dev/null
+++ b/extensions/proto/pom.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>com.google.truth.extensions</groupId>
+ <artifactId>truth-extensions-parent</artifactId>
+ <version>HEAD-SNAPSHOT</version>
+ </parent>
+ <artifactId>truth-proto-extension</artifactId>
+ <name>Truth Extension for Protocol Buffers</name>
+ <description>
+ An extension for the Truth test assertion framework supporting
+ Protocol Buffers.
+ </description>
+ <dependencies>
+ <dependency>
+ <groupId>com.google.truth</groupId>
+ <artifactId>truth</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.truth.extensions</groupId>
+ <artifactId>truth-liteproto-extension</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.checkerframework</groupId>
+ <artifactId>checker-qual</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.auto.value</groupId>
+ <artifactId>auto-value-annotations</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.errorprone</groupId>
+ <artifactId>error_prone_annotations</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.protobuf</groupId>
+ <artifactId>protobuf-java</artifactId>
+ </dependency>
+ </dependencies>
+ <build>
+ <extensions>
+ <extension>
+ <groupId>kr.motd.maven</groupId>
+ <artifactId>os-maven-plugin</artifactId>
+ <version>${os-maven-plugin.version}</version>
+ </extension>
+ </extensions>
+
+ <plugins>
+ <plugin>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.xolstice.maven.plugins</groupId>
+ <artifactId>protobuf-maven-plugin</artifactId>
+ <configuration>
+ <protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
+ <includes>
+ <param>test_message2.proto</param>
+ <param>test_message3.proto</param>
+ </includes>
+ </configuration>
+ <executions>
+ <execution>
+ <id>build-test-protos</id>
+ <phase>generate-test-sources</phase>
+ <goals>
+ <goal>test-compile</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <annotationProcessorPaths>
+ <path>
+ <groupId>com.google.auto.value</groupId>
+ <artifactId>auto-value</artifactId>
+ <version>${auto-value.version}</version>
+ </path>
+ </annotationProcessorPaths>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
+
+
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/AnyUtils.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/AnyUtils.java
new file mode 100644
index 00000000..34e1e3e7
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/AnyUtils.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2017 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.protobuf.Any;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.Descriptors.Descriptor;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.DynamicMessage;
+import com.google.protobuf.ExtensionRegistry;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.Message;
+import com.google.protobuf.TypeRegistry;
+
+/** Helper methods for working with Any protos. */
+class AnyUtils {
+ private static final FieldDescriptor TYPE_URL_FIELD_DESCRIPTOR =
+ Any.getDescriptor().findFieldByNumber(Any.TYPE_URL_FIELD_NUMBER);
+
+ static FieldDescriptor typeUrlFieldDescriptor() {
+ return TYPE_URL_FIELD_DESCRIPTOR;
+ }
+
+ private static final SubScopeId TYPE_URL_SUB_SCOPE_ID = SubScopeId.of(TYPE_URL_FIELD_DESCRIPTOR);
+
+ static SubScopeId typeUrlSubScopeId() {
+ return TYPE_URL_SUB_SCOPE_ID;
+ }
+
+ private static final FieldDescriptor VALUE_FIELD_DESCRIPTOR =
+ Any.getDescriptor().findFieldByNumber(Any.VALUE_FIELD_NUMBER);
+
+ static FieldDescriptor valueFieldDescriptor() {
+ return VALUE_FIELD_DESCRIPTOR;
+ }
+
+ private static final SubScopeId VALUE_SUB_SCOPE_ID = SubScopeId.of(VALUE_FIELD_DESCRIPTOR);
+
+ static SubScopeId valueSubScopeId() {
+ return VALUE_SUB_SCOPE_ID;
+ }
+
+ private static final TypeRegistry DEFAULT_TYPE_REGISTRY = TypeRegistry.getEmptyTypeRegistry();
+
+ static TypeRegistry defaultTypeRegistry() {
+ return DEFAULT_TYPE_REGISTRY;
+ }
+
+ private static final ExtensionRegistry DEFAULT_EXTENSION_REGISTRY =
+ ExtensionRegistry.getEmptyRegistry();
+
+ static ExtensionRegistry defaultExtensionRegistry() {
+ return DEFAULT_EXTENSION_REGISTRY;
+ }
+
+ /** Unpack an `Any` proto using the TypeRegistry and ExtensionRegistry on `config`. */
+ static Optional<Message> unpack(Message any, FluentEqualityConfig config) {
+ Preconditions.checkArgument(
+ any.getDescriptorForType().equals(Any.getDescriptor()),
+ "Expected type google.protobuf.Any, but was: %s",
+ any.getDescriptorForType().getFullName());
+
+ String typeUrl = (String) any.getField(typeUrlFieldDescriptor());
+ ByteString value = (ByteString) any.getField(valueFieldDescriptor());
+
+ try {
+ Descriptor descriptor = config.useTypeRegistry().getDescriptorForTypeUrl(typeUrl);
+ if (descriptor == null) {
+ return Optional.absent();
+ }
+
+ Message defaultMessage =
+ DynamicMessage.parseFrom(descriptor, value, config.useExtensionRegistry());
+ return Optional.of(defaultMessage);
+ } catch (InvalidProtocolBufferException e) {
+ return Optional.absent();
+ }
+ }
+
+ private AnyUtils() {}
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/DiffResult.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/DiffResult.java
new file mode 100644
index 00000000..a0ec9373
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/DiffResult.java
@@ -0,0 +1,664 @@
+/*
+ * Copyright (c) 2017 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import com.google.auto.value.AutoValue;
+import com.google.auto.value.extension.memoized.Memoized;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Ordering;
+import com.google.common.collect.Sets;
+import com.google.common.truth.extensions.proto.RecursableDiffEntity.WithResultCode.Result;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.ForOverride;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.Message;
+import com.google.protobuf.TextFormat;
+import com.google.protobuf.UnknownFieldSet;
+import java.io.IOException;
+import java.util.Set;
+
+/**
+ * Structural summary of the difference between two messages.
+ *
+ * <p>A {@code DiffResult} has singular fields, repeated fields, and unknowns, each with their own
+ * class. The inner classes may also contain their own {@code DiffResult}s for submessages.
+ *
+ * <p>These classes form a recursive, hierarchical relationship. Much of the common recursion logic
+ * across all the classes is in {@link RecursableDiffEntity}.
+ */
+@AutoValue
+abstract class DiffResult extends RecursableDiffEntity.WithoutResultCode {
+ /**
+ * Structural summary of the difference between two singular (non-repeated) fields.
+ *
+ * <p>It is possible for the result to be {@code ADDED} or {@code REMOVED}, even if {@code
+ * actual()} and {@code expected()} are identical. This occurs if the config does not have {@link
+ * FluentEqualityConfig#ignoringFieldAbsence()} enabled, one message had the field explicitly set
+ * to the default value, and the other did not.
+ */
+ @AutoValue
+ abstract static class SingularField extends RecursableDiffEntity.WithResultCode {
+ /** The type information for this field. May be absent if result code is {@code IGNORED}. */
+ abstract Optional<SubScopeId> subScopeId();
+
+ /** The display name for this field. May include an array-index specifier. */
+ abstract String fieldName();
+
+ /** The field under test. */
+ abstract Optional<Object> actual();
+
+ /** The expected value of said field. */
+ abstract Optional<Object> expected();
+
+ /**
+ * The detailed breakdown of the comparison, only present if both objects are set on this
+ * instance and they are messages.
+ *
+ * <p>This does not necessarily mean the messages were set on the input protos.
+ */
+ abstract Optional<DiffResult> breakdown();
+
+ /**
+ * The detailed breakdown of the comparison, only present if both objects are set and they are
+ * {@link UnknownFieldSet}s.
+ *
+ * <p>This will only ever be set inside a parent {@link UnknownFieldSetDiff}. The top {@link
+ * UnknownFieldSetDiff} is set on the {@link DiffResult}, not here.
+ */
+ abstract Optional<UnknownFieldSetDiff> unknownsBreakdown();
+
+ /** Returns {@code actual().get()}, or {@code expected().get()}, whichever is available. */
+ @Memoized
+ Object actualOrExpected() {
+ return actual().or(expected()).get();
+ }
+
+ @Memoized
+ @Override
+ Iterable<? extends RecursableDiffEntity> childEntities() {
+ return ImmutableList.copyOf(
+ Iterables.concat(breakdown().asSet(), unknownsBreakdown().asSet()));
+ }
+
+ @Override
+ final void printContents(boolean includeMatches, String fieldPrefix, StringBuilder sb) {
+ if (!includeMatches && isMatched()) {
+ return;
+ }
+
+ fieldPrefix = newFieldPrefix(fieldPrefix, fieldName());
+ switch (result()) {
+ case ADDED:
+ sb.append("added: ").append(fieldPrefix).append(": ");
+ if (actual().get() instanceof Message) {
+ sb.append("\n").append(actual().get());
+ } else {
+ sb.append(valueString(subScopeId().get(), actual().get())).append("\n");
+ }
+ return;
+ case IGNORED:
+ sb.append("ignored: ").append(fieldPrefix).append("\n");
+ return;
+ case MATCHED:
+ sb.append("matched: ").append(fieldPrefix);
+ if (actualOrExpected() instanceof Message) {
+ sb.append("\n");
+ printChildContents(includeMatches, fieldPrefix, sb);
+ } else {
+ sb.append(": ")
+ .append(valueString(subScopeId().get(), actualOrExpected()))
+ .append("\n");
+ }
+ return;
+ case MODIFIED:
+ sb.append("modified: ").append(fieldPrefix);
+ if (actualOrExpected() instanceof Message) {
+ sb.append("\n");
+ printChildContents(includeMatches, fieldPrefix, sb);
+ } else {
+ sb.append(": ")
+ .append(valueString(subScopeId().get(), expected().get()))
+ .append(" -> ")
+ .append(valueString(subScopeId().get(), actual().get()))
+ .append("\n");
+ }
+ return;
+ case REMOVED:
+ sb.append("deleted: ").append(fieldPrefix).append(": ");
+ if (expected().get() instanceof Message) {
+ sb.append("\n").append(expected().get());
+ } else {
+ sb.append(valueString(subScopeId().get(), expected().get())).append("\n");
+ }
+ return;
+ default:
+ throw new AssertionError("Impossible: " + result());
+ }
+ }
+
+ @Override
+ final boolean isContentEmpty() {
+ return false;
+ }
+
+ static SingularField ignored(String fieldName) {
+ return newBuilder().setFieldName(fieldName).setResult(Result.IGNORED).build();
+ }
+
+ static Builder newBuilder() {
+ return new AutoValue_DiffResult_SingularField.Builder();
+ }
+
+ /** Builder for {@link SingularField}. */
+ @CanIgnoreReturnValue
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder setResult(Result result);
+
+ abstract Builder setSubScopeId(SubScopeId subScopeId);
+
+ abstract Builder setFieldName(String fieldName);
+
+ abstract Builder setActual(Object actual);
+
+ abstract Builder setExpected(Object expected);
+
+ abstract Builder setBreakdown(DiffResult breakdown);
+
+ abstract Builder setUnknownsBreakdown(UnknownFieldSetDiff unknownsBreakdown);
+
+ abstract SingularField build();
+ }
+ }
+
+ /**
+ * Structural summary of the difference between two repeated fields.
+ *
+ * <p>This is only present if the user specified {@code ignoringRepeatedFieldOrder()}. Otherwise,
+ * the repeated elements are compared as singular fields, and there are no 'move' semantics.
+ */
+ @AutoValue
+ abstract static class RepeatedField extends RecursableDiffEntity.WithoutResultCode {
+
+ /**
+ * Structural summary of the difference between two elements in two corresponding repeated
+ * fields, in the context of a {@link RepeatedField} diff.
+ *
+ * <p>The field indexes will only be present if the corresponding object is also present. If an
+ * object is absent, the PairResult represents an extra/missing element in the repeated field.
+ * If both are present but the indexes differ, it represents a 'move'.
+ */
+ @AutoValue
+ abstract static class PairResult extends RecursableDiffEntity.WithResultCode {
+ /** The {@link FieldDescriptor} describing the repeated field for this pair. */
+ abstract FieldDescriptor fieldDescriptor();
+
+ /** The index of the element in the {@code actual()} list that was matched. */
+ abstract Optional<Integer> actualFieldIndex();
+
+ /** The index of the element in the {@code expected()} list that was matched. */
+ abstract Optional<Integer> expectedFieldIndex();
+
+ /** The element in the {@code actual()} list that was matched. */
+ abstract Optional<Object> actual();
+
+ /** The element in the {@code expected()} list that was matched. */
+ abstract Optional<Object> expected();
+
+ /**
+ * A detailed breakdown of the comparison between the messages. Present iff {@code actual()}
+ * and {@code expected()} are {@link Message}s.
+ */
+ abstract Optional<DiffResult> breakdown();
+
+ @Memoized
+ @Override
+ Iterable<? extends RecursableDiffEntity> childEntities() {
+ return breakdown().asSet();
+ }
+
+ /** Returns true if actual() and expected() contain Message types. */
+ @Memoized
+ boolean isMessage() {
+ return actual().orNull() instanceof Message || expected().orNull() instanceof Message;
+ }
+
+ private static String indexed(String fieldPrefix, Optional<Integer> fieldIndex) {
+ String index = fieldIndex.isPresent() ? fieldIndex.get().toString() : "?";
+ return fieldPrefix + "[" + index + "]";
+ }
+
+ @Override
+ final void printContents(boolean includeMatches, String fieldPrefix, StringBuilder sb) {
+ printContentsForRepeatedField(
+ /* includeSelfAlways = */ false, includeMatches, fieldPrefix, sb);
+ }
+
+ // When printing results for a repeated field, we want to print matches even if
+ // !includeMatches if there's a mismatch on the repeated field itself, but not recursively.
+ // So we define a second printing method for use by the parent.
+ final void printContentsForRepeatedField(
+ boolean includeSelfAlways, boolean includeMatches, String fieldPrefix, StringBuilder sb) {
+ if (!includeSelfAlways && !includeMatches && isMatched()) {
+ return;
+ }
+
+ switch (result()) {
+ case ADDED:
+ sb.append("added: ").append(indexed(fieldPrefix, actualFieldIndex())).append(": ");
+ if (isMessage()) {
+ sb.append("\n").append(actual().get());
+ } else {
+ sb.append(valueString(fieldDescriptor(), actual().get())).append("\n");
+ }
+ return;
+ case IGNORED:
+ sb.append("ignored: ");
+ if (actualFieldIndex().equals(expectedFieldIndex())) {
+ sb.append(indexed(fieldPrefix, actualFieldIndex()));
+ } else {
+ sb.append(indexed(fieldPrefix, expectedFieldIndex()))
+ .append(" -> ")
+ .append(indexed(fieldPrefix, actualFieldIndex()));
+ }
+
+ // We output the message contents for ignored pair results, since it's likely not clear
+ // from the index alone why they were ignored.
+ sb.append(":");
+ if (isMessage()) {
+ sb.append("\n");
+ printChildContents(includeMatches, indexed(fieldPrefix, actualFieldIndex()), sb);
+ } else {
+ sb.append(" ").append(valueString(fieldDescriptor(), actual().get())).append("\n");
+ }
+ return;
+ case MATCHED:
+ if (actualFieldIndex().get().equals(expectedFieldIndex().get())) {
+ sb.append("matched: ").append(indexed(fieldPrefix, actualFieldIndex()));
+ } else {
+ sb.append("moved: ")
+ .append(indexed(fieldPrefix, expectedFieldIndex()))
+ .append(" -> ")
+ .append(indexed(fieldPrefix, actualFieldIndex()));
+ }
+ sb.append(":");
+ if (isMessage()) {
+ sb.append("\n");
+ printChildContents(includeMatches, indexed(fieldPrefix, actualFieldIndex()), sb);
+ } else {
+ sb.append(" ").append(valueString(fieldDescriptor(), actual().get())).append("\n");
+ }
+ return;
+ case MOVED_OUT_OF_ORDER:
+ sb.append("out_of_order: ")
+ .append(indexed(fieldPrefix, expectedFieldIndex()))
+ .append(" -> ")
+ .append(indexed(fieldPrefix, actualFieldIndex()));
+ sb.append(":");
+ if (isMessage()) {
+ sb.append("\n");
+ printChildContents(includeMatches, indexed(fieldPrefix, actualFieldIndex()), sb);
+ } else {
+ sb.append(" ").append(valueString(fieldDescriptor(), actual().get())).append("\n");
+ }
+ return;
+ case MODIFIED:
+ sb.append("modified: ");
+ if (actualFieldIndex().get().equals(expectedFieldIndex().get())) {
+ sb.append(indexed(fieldPrefix, actualFieldIndex()));
+ } else {
+ sb.append(indexed(fieldPrefix, expectedFieldIndex()))
+ .append(" -> ")
+ .append(indexed(fieldPrefix, actualFieldIndex()));
+ }
+ sb.append(":");
+ if (isMessage()) {
+ sb.append("\n");
+ printChildContents(includeMatches, indexed(fieldPrefix, actualFieldIndex()), sb);
+ } else {
+ sb.append(" ")
+ .append(valueString(fieldDescriptor(), expected().get()))
+ .append(" -> ")
+ .append(valueString(fieldDescriptor(), actual().get()));
+ }
+ return;
+ case REMOVED:
+ sb.append("deleted: ").append(indexed(fieldPrefix, expectedFieldIndex())).append(": ");
+ if (isMessage()) {
+ sb.append("\n").append(expected().get());
+ } else {
+ sb.append(valueString(fieldDescriptor(), expected().get())).append("\n");
+ }
+ return;
+ }
+ throw new AssertionError("Impossible: " + result());
+ }
+
+ @Override
+ final boolean isContentEmpty() {
+ return false;
+ }
+
+ abstract Builder toBuilder();
+
+ static Builder newBuilder() {
+ return new AutoValue_DiffResult_RepeatedField_PairResult.Builder();
+ }
+
+ @CanIgnoreReturnValue
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder setResult(Result result);
+
+ abstract Builder setFieldDescriptor(FieldDescriptor fieldDescriptor);
+
+ abstract Builder setActualFieldIndex(int actualFieldIndex);
+
+ abstract Builder setExpectedFieldIndex(int expectedFieldIndex);
+
+ abstract Builder setActual(Object actual);
+
+ abstract Builder setExpected(Object expected);
+
+ abstract Builder setBreakdown(DiffResult breakdown);
+
+ abstract PairResult build();
+ }
+ }
+
+ /** The {@link FieldDescriptor} for this repeated field. */
+ abstract FieldDescriptor fieldDescriptor();
+
+ /** The elements under test. */
+ abstract ImmutableList<Object> actual();
+
+ /** The elements expected. */
+ abstract ImmutableList<Object> expected();
+
+ // TODO(user,peteg): Also provide a minimum-edit-distance pairing between unmatched elements,
+ // and the diff report between them.
+
+ /** Pairs of elements which were diffed against each other. */
+ abstract ImmutableList<PairResult> pairResults();
+
+ @Memoized
+ @Override
+ Iterable<? extends RecursableDiffEntity> childEntities() {
+ return pairResults();
+ }
+
+ @Override
+ final void printContents(boolean includeMatches, String fieldPrefix, StringBuilder sb) {
+ fieldPrefix = newFieldPrefix(fieldPrefix, fieldDescriptor().getName());
+ for (PairResult pairResult : pairResults()) {
+ pairResult.printContentsForRepeatedField(
+ /* includeSelfAlways = */ !isMatched(), includeMatches, fieldPrefix, sb);
+ }
+ }
+
+ @Override
+ final boolean isContentEmpty() {
+ return pairResults().isEmpty();
+ }
+
+ static Builder newBuilder() {
+ return new AutoValue_DiffResult_RepeatedField.Builder();
+ }
+
+ @CanIgnoreReturnValue
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder setFieldDescriptor(FieldDescriptor fieldDescriptor);
+
+ abstract Builder setActual(Iterable<?> actual);
+
+ abstract Builder setExpected(Iterable<?> expected);
+
+ @ForOverride
+ abstract ImmutableList.Builder<PairResult> pairResultsBuilder();
+
+ final Builder addPairResult(PairResult pairResult) {
+ pairResultsBuilder().add(pairResult);
+ return this;
+ }
+
+ abstract RepeatedField build();
+ }
+ }
+
+ /** Structural summary of the difference between two unknown field sets. */
+ @AutoValue
+ abstract static class UnknownFieldSetDiff extends RecursableDiffEntity.WithoutResultCode {
+ /** The {@link UnknownFieldSet} being tested. */
+ abstract Optional<UnknownFieldSet> actual();
+
+ /** The {@link UnknownFieldSet} expected. */
+ abstract Optional<UnknownFieldSet> expected();
+
+ /**
+ * A list of top-level singular field comparison results.
+ *
+ * <p>All unknown fields are treated as repeated and with {@code ignoringRepeatedFieldOrder()}
+ * off, because we don't know their nature. If they're optional, only last element matters, but
+ * if they're repeated, all elements matter.
+ */
+ abstract ImmutableListMultimap<Integer, SingularField> singularFields();
+
+ @Memoized
+ @Override
+ Iterable<? extends RecursableDiffEntity> childEntities() {
+ return singularFields().values();
+ }
+
+ @Override
+ final void printContents(boolean includeMatches, String fieldPrefix, StringBuilder sb) {
+ if (!includeMatches && isMatched()) {
+ return;
+ }
+
+ for (int fieldNumber : singularFields().keySet()) {
+ for (SingularField singularField : singularFields().get(fieldNumber)) {
+ singularField.printContents(includeMatches, fieldPrefix, sb);
+ }
+ }
+ }
+
+ @Override
+ final boolean isContentEmpty() {
+ return singularFields().isEmpty();
+ }
+
+ static Builder newBuilder() {
+ return new AutoValue_DiffResult_UnknownFieldSetDiff.Builder();
+ }
+
+ @CanIgnoreReturnValue
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder setActual(UnknownFieldSet actual);
+
+ abstract Builder setExpected(UnknownFieldSet expected);
+
+ @ForOverride
+ abstract ImmutableListMultimap.Builder<Integer, SingularField> singularFieldsBuilder();
+
+ final Builder addSingularField(int fieldNumber, SingularField singularField) {
+ singularFieldsBuilder().put(fieldNumber, singularField);
+ return this;
+ }
+
+ final Builder addAllSingularFields(int fieldNumber, Iterable<SingularField> singularFields) {
+ singularFieldsBuilder().putAll(fieldNumber, singularFields);
+ return this;
+ }
+
+ abstract UnknownFieldSetDiff build();
+ }
+ }
+
+ /** The {@link Message} being tested. */
+ abstract Message actual();
+
+ /** The {@link Message} expected. */
+ abstract Message expected();
+
+ /** A list of top-level singular field comparison results grouped by field number. */
+ abstract ImmutableListMultimap<Integer, SingularField> singularFields();
+
+ /**
+ * A list of top-level repeated field comparison results grouped by field number.
+ *
+ * <p>This is only populated if {@link FluentEqualityConfig#ignoreRepeatedFieldOrder()} is set.
+ * Otherwise, repeated fields are compared strictly in index order, as singular fields.
+ */
+ abstract ImmutableListMultimap<Integer, RepeatedField> repeatedFields();
+
+ /**
+ * The result of comparing the message's {@link UnknownFieldSet}s. Not present if unknown fields
+ * were not compared.
+ */
+ abstract Optional<UnknownFieldSetDiff> unknownFields();
+
+ @Memoized
+ @Override
+ Iterable<? extends RecursableDiffEntity> childEntities() {
+ // Assemble the diffs in field number order so it most closely matches the schema.
+ ImmutableList.Builder<RecursableDiffEntity> builder =
+ ImmutableList.builderWithExpectedSize(
+ singularFields().size() + repeatedFields().size() + unknownFields().asSet().size());
+ Set<Integer> fieldNumbers = Sets.union(singularFields().keySet(), repeatedFields().keySet());
+ for (int fieldNumber : Ordering.natural().sortedCopy(fieldNumbers)) {
+ builder.addAll(singularFields().get(fieldNumber));
+ builder.addAll(repeatedFields().get(fieldNumber));
+ }
+ builder.addAll(unknownFields().asSet());
+ return builder.build();
+ }
+
+ /** Prints the full {@link DiffResult} to a human-readable string, for use in test outputs. */
+ final String printToString(boolean reportMismatchesOnly) {
+ StringBuilder sb = new StringBuilder();
+
+ if (!isMatched()) {
+ sb.append("Differences were found:\n");
+ printContents(/* includeMatches = */ false, /* fieldPrefix = */ "", sb);
+
+ if (!reportMismatchesOnly && isAnyChildMatched()) {
+ sb.append("\nFull diff report:\n");
+ printContents(/* includeMatches = */ true, /* fieldPrefix = */ "", sb);
+ }
+ } else {
+ sb.append("No differences were found.");
+ if (!reportMismatchesOnly) {
+ if (isAnyChildIgnored()) {
+ sb.append("\nSome fields were ignored for comparison, however.\n");
+ } else {
+ sb.append("\nFull diff report:\n");
+ }
+ printContents(/* includeMatches = */ true, /* fieldPrefix = */ "", sb);
+ }
+ }
+
+ return sb.toString();
+ }
+
+ @Override
+ final void printContents(boolean includeMatches, String fieldPrefix, StringBuilder sb) {
+ for (RecursableDiffEntity child : childEntities()) {
+ child.printContents(includeMatches, fieldPrefix, sb);
+ }
+ }
+
+ @Override
+ final boolean isContentEmpty() {
+ return Iterables.isEmpty(childEntities());
+ }
+
+ static Builder newBuilder() {
+ return new AutoValue_DiffResult.Builder();
+ }
+
+ private static String newFieldPrefix(String rootFieldPrefix, String toAdd) {
+ return rootFieldPrefix.isEmpty() ? toAdd : (rootFieldPrefix + "." + toAdd);
+ }
+
+ private static String valueString(SubScopeId subScopeId, Object o) {
+ switch (subScopeId.kind()) {
+ case FIELD_DESCRIPTOR:
+ return valueString(subScopeId.fieldDescriptor(), o);
+ case UNKNOWN_FIELD_DESCRIPTOR:
+ return valueString(subScopeId.unknownFieldDescriptor(), o);
+ }
+ throw new AssertionError(subScopeId.kind());
+ }
+
+ private static String valueString(FieldDescriptor fieldDescriptor, Object o) {
+ StringBuilder sb = new StringBuilder();
+ try {
+ TextFormat.printFieldValue(fieldDescriptor, o, sb);
+ return sb.toString();
+ } catch (IOException impossible) {
+ throw new AssertionError(impossible);
+ }
+ }
+
+ private static String valueString(UnknownFieldDescriptor unknownFieldDescriptor, Object o) {
+ StringBuilder sb = new StringBuilder();
+ try {
+ TextFormat.printUnknownFieldValue(unknownFieldDescriptor.type().wireType(), o, sb);
+ return sb.toString();
+ } catch (IOException impossible) {
+ throw new AssertionError(impossible);
+ }
+ }
+
+ @CanIgnoreReturnValue
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder setActual(Message actual);
+
+ abstract Builder setExpected(Message expected);
+
+ @ForOverride
+ abstract ImmutableListMultimap.Builder<Integer, SingularField> singularFieldsBuilder();
+
+ final Builder addSingularField(int fieldNumber, SingularField singularField) {
+ singularFieldsBuilder().put(fieldNumber, singularField);
+ return this;
+ }
+
+ final Builder addAllSingularFields(int fieldNumber, Iterable<SingularField> singularFields) {
+ singularFieldsBuilder().putAll(fieldNumber, singularFields);
+ return this;
+ }
+
+ @ForOverride
+ abstract ImmutableListMultimap.Builder<Integer, RepeatedField> repeatedFieldsBuilder();
+
+ final Builder addRepeatedField(int fieldNumber, RepeatedField repeatedField) {
+ repeatedFieldsBuilder().put(fieldNumber, repeatedField);
+ return this;
+ }
+
+ abstract Builder setUnknownFields(UnknownFieldSetDiff unknownFields);
+
+ abstract DiffResult build();
+ }
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldDescriptorValidator.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldDescriptorValidator.java
new file mode 100644
index 00000000..a0d64ced
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldDescriptorValidator.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2018 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.Descriptors.FieldDescriptor.JavaType;
+import com.google.protobuf.Descriptors.FileDescriptor.Syntax;
+
+/** Various validators, to ensure that explicit comparison settings made by the user make sense. */
+enum FieldDescriptorValidator {
+ ALLOW_ALL() {
+ @Override
+ void validate(FieldDescriptor fieldDescriptor) {}
+ },
+ IS_FIELD_WITH_ABSENCE() {
+ @Override
+ void validate(FieldDescriptor fieldDescriptor) {
+ checkArgument(
+ !fieldDescriptor.isRepeated(),
+ "%s is a repeated field; repeated fields cannot be absent, only empty",
+ fieldDescriptor);
+
+ checkArgument(
+ fieldDescriptor.getContainingType().getFile().getSyntax() != Syntax.PROTO3
+ || fieldDescriptor.getJavaType() == JavaType.MESSAGE,
+ "%s is a primitive field in a Proto 3 message; it cannot be absent",
+ fieldDescriptor);
+ }
+ },
+ IS_FIELD_WITH_ORDER() {
+ @Override
+ void validate(FieldDescriptor fieldDescriptor) {
+ checkArgument(
+ !fieldDescriptor.isMapField(), "%s is a map field; it has no order", fieldDescriptor);
+ checkArgument(
+ fieldDescriptor.isRepeated(),
+ "%s is not a repeated field; it has no order",
+ fieldDescriptor);
+ }
+ },
+ IS_FIELD_WITH_EXTRA_ELEMENTS() {
+ @Override
+ void validate(FieldDescriptor fieldDescriptor) {
+ checkArgument(
+ fieldDescriptor.isRepeated(),
+ "%s is not a repeated field or a map field; it cannot contain extra elements",
+ fieldDescriptor);
+ }
+ },
+ IS_DOUBLE_FIELD() {
+ @Override
+ void validate(FieldDescriptor fieldDescriptor) {
+ checkArgument(
+ fieldDescriptor.getJavaType() == JavaType.DOUBLE,
+ "%s is not a double field",
+ fieldDescriptor);
+ }
+ },
+ IS_FLOAT_FIELD() {
+ @Override
+ void validate(FieldDescriptor fieldDescriptor) {
+ checkArgument(
+ fieldDescriptor.getJavaType() == JavaType.FLOAT,
+ "%s is not a float field",
+ fieldDescriptor);
+ }
+ };
+
+ /** Validates the given {@link FieldDescriptor} according to this instance's rules. */
+ abstract void validate(FieldDescriptor fieldDescriptor);
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldNumberTree.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldNumberTree.java
new file mode 100644
index 00000000..d33e2fee
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldNumberTree.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import com.google.common.collect.Maps;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.Message;
+import com.google.protobuf.UnknownFieldSet;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Tree representation of all set field numbers in a message, merging across repeated elements.
+ *
+ * <p>Sub messages are represented by child {@link FieldNumberTree} objects.
+ *
+ * @see FieldScopeImpl#partialScope
+ */
+final class FieldNumberTree {
+
+ private static final FieldNumberTree EMPTY = new FieldNumberTree();
+
+ /** A {@code FieldNumberTree} with no children. */
+ static FieldNumberTree empty() {
+ return EMPTY;
+ }
+
+ // Modified only during [factory] construction, never changed afterwards.
+ private final Map<SubScopeId, FieldNumberTree> children = Maps.newHashMap();
+
+ /** Returns whether this {@code FieldNumberTree} has no children. */
+ boolean isEmpty() {
+ return children.isEmpty();
+ }
+
+ /**
+ * Returns the {@code FieldNumberTree} corresponding to this sub-field.
+ *
+ * <p>{@code empty()} if there is none.
+ */
+ FieldNumberTree child(SubScopeId subScopeId) {
+ FieldNumberTree child = children.get(subScopeId);
+ return child == null ? EMPTY : child;
+ }
+
+ /** Returns whether this tree has a child for this node. */
+ boolean hasChild(SubScopeId subScopeId) {
+ return children.containsKey(subScopeId);
+ }
+
+ static FieldNumberTree fromMessage(Message message) {
+ FieldNumberTree tree = new FieldNumberTree();
+
+ // Known fields.
+ Map<FieldDescriptor, Object> knownFieldValues = message.getAllFields();
+ for (FieldDescriptor field : knownFieldValues.keySet()) {
+ SubScopeId subScopeId = SubScopeId.of(field);
+ FieldNumberTree childTree = new FieldNumberTree();
+ tree.children.put(subScopeId, childTree);
+
+ Object fieldValue = knownFieldValues.get(field);
+ if (field.getJavaType() == FieldDescriptor.JavaType.MESSAGE) {
+ if (field.isRepeated()) {
+ List<?> valueList = (List<?>) fieldValue;
+ for (Object value : valueList) {
+ childTree.merge(fromMessage((Message) value));
+ }
+ } else {
+ childTree.merge(fromMessage((Message) fieldValue));
+ }
+ }
+ }
+
+ // Unknown fields.
+ tree.merge(fromUnknownFieldSet(message.getUnknownFields()));
+
+ return tree;
+ }
+
+ static FieldNumberTree fromMessages(Iterable<? extends Message> messages) {
+ FieldNumberTree tree = new FieldNumberTree();
+ for (Message message : messages) {
+ if (message != null) {
+ tree.merge(fromMessage(message));
+ }
+ }
+ return tree;
+ }
+
+ private static FieldNumberTree fromUnknownFieldSet(UnknownFieldSet unknownFieldSet) {
+ FieldNumberTree tree = new FieldNumberTree();
+ for (int fieldNumber : unknownFieldSet.asMap().keySet()) {
+ UnknownFieldSet.Field unknownField = unknownFieldSet.asMap().get(fieldNumber);
+ for (UnknownFieldDescriptor unknownFieldDescriptor :
+ UnknownFieldDescriptor.descriptors(fieldNumber, unknownField)) {
+ SubScopeId subScopeId = SubScopeId.of(unknownFieldDescriptor);
+ FieldNumberTree childTree = new FieldNumberTree();
+ tree.children.put(subScopeId, childTree);
+
+ if (unknownFieldDescriptor.type() == UnknownFieldDescriptor.Type.GROUP) {
+ for (Object group : unknownFieldDescriptor.type().getValues(unknownField)) {
+ childTree.merge(fromUnknownFieldSet((UnknownFieldSet) group));
+ }
+ }
+ }
+ }
+
+ return tree;
+ }
+
+ /** Adds the other tree onto this one. May destroy {@code other} in the process. */
+ private void merge(FieldNumberTree other) {
+ for (SubScopeId subScopeId : other.children.keySet()) {
+ FieldNumberTree value = other.children.get(subScopeId);
+ if (!this.children.containsKey(subScopeId)) {
+ this.children.put(subScopeId, value);
+ } else {
+ this.children.get(subScopeId).merge(value);
+ }
+ }
+ }
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScope.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScope.java
new file mode 100644
index 00000000..17b18982
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScope.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import com.google.common.base.Optional;
+import com.google.protobuf.Descriptors.Descriptor;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+
+/**
+ * An immutable, abstract representation of a set of specific field paths. See {@link FieldScopes}
+ * for entry points to obtain a {@code FieldScope} object.
+ *
+ * <p>A {@code FieldScope} is similar in concept to a {@code FieldMask}, which is an explicitly
+ * enumerated set of specific field paths. A FieldScope is more general, allowing for the
+ * description of arbitrary classes of specific field paths to be included or excluded from its
+ * definition. For example, given a large protocol buffer with many field definitions, and a single
+ * string field named 'x', it is arduous to specify "All fields except 'x'" as a {@code FieldMask}.
+ * With a {@code FieldScope}, it is simply {@code
+ * FieldScopes.ignoringFields(MyMessage.X_FIELD_NUMBER)}.
+ *
+ * <p>All inclusion and exclusion operations on message-type fields are recursive, but may be
+ * overridden by subsequent operations. In this way, a complex {@code FieldScope} such as:
+ *
+ * <pre>{@code
+ * FieldScopes.ignoringFields(A.B_FIELD_NUMBER)
+ * .allowingFieldDescriptors(B.getDescriptor().findFieldByName("flag"))
+ * }</pre>
+ *
+ * ...will match all fields on A, except fields on the message type B, but including B's flag field.
+ * Thus, two messages of type A will compare equal even if their sub messages of type B are
+ * completely different, so long as the 'flag' fields for each B matches. Because of this, method
+ * ordering matters. Generally, exclusions should come after inclusions.
+ *
+ * <p>{@code FieldScope}s are not designed to be compact or efficient, trading flexibility of use
+ * for runtime efficiency, generally composing themselves as recursive structures. For this reason,
+ * it is not recommended to use {@code FieldScope} in production code. Prefer to use proper {@code
+ * FieldMask}s, directly in production code.
+ */
+public abstract class FieldScope {
+
+ /**
+ * Returns a {@code FieldScope} equivalent to this one, minus all fields defined by the given
+ * field numbers.
+ *
+ * <p>Validation of the field numbers is performed when the {@code FieldScope} is invoked
+ * (typically by {@link ProtoFluentAssertion#isEqualTo}). A runtime exception will occur if bad
+ * field numbers are encountered.
+ *
+ * <p>The field numbers are ignored recursively on this type. That is, if {@code YourMessage}
+ * contains another {@code YourMessage} somewhere within its subtree, a {@code FieldScope
+ * ignoringFields(X)} will ignore field number {@code X} for all submessages of type {@code
+ * YourMessage}, as well as for the top-level message.
+ */
+ public abstract FieldScope ignoringFields(int firstFieldNumber, int... rest);
+
+ /**
+ * Returns a {@code FieldScope} equivalent to this one, minus all fields defined by the given
+ * field numbers.
+ *
+ * <p>Validation of the field numbers is performed when the {@code FieldScope} is invoked
+ * (typically by {@link ProtoFluentAssertion#isEqualTo}). A runtime exception will occur if bad
+ * field numbers are encountered.
+ *
+ * <p>The field numbers are ignored recursively on this type. That is, if {@code YourMessage}
+ * contains another {@code YourMessage} somewhere within its subtree, a {@code FieldScope
+ * ignoringFields(X)} will ignore field number {@code X} for all submessages of type {@code
+ * YourMessage}, as well as for the top-level message.
+ */
+ public abstract FieldScope ignoringFields(Iterable<Integer> fieldNumbers);
+
+ /**
+ * Returns a {@code FieldScope} equivalent to this one, minus all fields matching the given {@link
+ * FieldDescriptor}s.
+ *
+ * <p>The {@link FieldDescriptor}s are not validated, as that would require scanning the entire
+ * protobuf schema recursively from this message type. If a {@link FieldDescriptor} is provided
+ * which refers to a field that is not part of this message, or any possible recursive
+ * submessages, it is silently ignored.
+ *
+ * <p>The field descriptors are also ignored recursively on the message type. That is, if {@code
+ * FooMessage.field_bar} is ignored, {@code field_bar} will be ignored for all submessages of the
+ * parent type of type {@code FooMessage}.
+ */
+ public abstract FieldScope ignoringFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Returns a {@code FieldScope} equivalent to this one, minus all fields defined by the given
+ * field numbers.
+ *
+ * <p>The {@link FieldDescriptor}s are not validated, as that would require scanning the entire
+ * protobuf schema recursively from this message type. If a {@link FieldDescriptor} is provided
+ * which refers to a field that is not part of this message, or any possible recursive
+ * submessages, it is silently ignored.
+ *
+ * <p>The field descriptors are also ignored recursively on the message type. That is, if {@code
+ * FooMessage.field_bar} is ignored, {@code field_bar} will be ignored for all submessages of the
+ * parent type of type {@code FooMessage}.
+ */
+ public abstract FieldScope ignoringFieldDescriptors(Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Returns a {@code FieldScope} equivalent to this one, plus all fields defined by the given field
+ * numbers.
+ *
+ * <p>Validation of the field numbers is performed when the {@code FieldScope} is invoked
+ * (typically by {@link ProtoFluentAssertion#isEqualTo}). A runtime exception will occur if bad
+ * field numbers are encountered.
+ *
+ * <p>The field numbers are included recursively on this type. That is, if {@code YourMessage}
+ * contains another {@code YourMessage} somewhere within its subtree, a {@code FieldScope
+ * allowingFields(X)} will include field number {@code X} for all submessages of type {@code
+ * YourMessage}, as well as for the top-level message.
+ */
+ public abstract FieldScope allowingFields(int firstFieldNumber, int... rest);
+
+ /**
+ * Returns a {@code FieldScope} equivalent to this one, plus all fields defined by the given field
+ * numbers.
+ *
+ * <p>Validation of the field numbers is performed when the {@code FieldScope} is invoked
+ * (typically by {@link ProtoFluentAssertion#isEqualTo}). A runtime exception will occur if bad
+ * field numbers are encountered.
+ *
+ * <p>The field numbers are included recursively on this type. That is, if {@code YourMessage}
+ * contains another {@code YourMessage} somewhere within its subtree, a {@code FieldScope
+ * allowingFields(X)} will include field number {@code X} for all submessages of type {@code
+ * YourMessage}, as well as for the top-level message.
+ */
+ public abstract FieldScope allowingFields(Iterable<Integer> fieldNumbers);
+
+ /**
+ * Returns a {@code FieldScope} equivalent to this one, plus all fields matching the given {@link
+ * FieldDescriptor}s.
+ *
+ * <p>The {@link FieldDescriptor}s are not validated, as that would require scanning the entire
+ * protobuf schema from this message type. If a {@link FieldDescriptor} is provided which refers
+ * to a field that is not part of this message, or any possible recursive submessages, it is
+ * silently ignored.
+ *
+ * <p>The field descriptors are also included recursively on the message type. That is, if {@code
+ * FooMessage.field_bar} is included, {@code field_bar} will be included for all submessages of
+ * the parent type of type {@code FooMessage}.
+ */
+ public abstract FieldScope allowingFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Returns a {@code FieldScope} equivalent to this one, plus all fields matching the given {@link
+ * FieldDescriptor}s.
+ *
+ * <p>The {@link FieldDescriptor}s are not validated, as that would require scanning the entire
+ * protobuf schema from this message type. If a {@link FieldDescriptor} is provided which refers
+ * to a field that is not part of this message, or any possible recursive submessages, it is
+ * silently ignored.
+ *
+ * <p>The field descriptors are also included recursively on the message type. That is, if {@code
+ * FooMessage.field_bar} is included, {@code field_bar} will be included for all submessages of
+ * the parent type of type {@code FooMessage}.
+ */
+ public abstract FieldScope allowingFieldDescriptors(Iterable<FieldDescriptor> fieldDescriptors);
+
+ // package-protected: Should not be implemented outside the package.
+ FieldScope() {}
+
+ /** Returns the underlying logical implementation of the {@link FieldScope}. */
+ abstract FieldScopeLogic logic();
+
+ /**
+ * Returns a human-readable representation of this {@link FieldScope}, detailing its construction.
+ *
+ * <p>For use in {@link com.google.common.truth.Correspondence#toString()} for clarity.
+ *
+ * @param descriptor a unique message {@link Descriptor} that applies to all non-null arguments,
+ * if present. Used to pretty-print raw field numbers.
+ */
+ abstract String usingCorrespondenceString(Optional<Descriptor> descriptor);
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeImpl.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeImpl.java
new file mode 100644
index 00000000..4acf9916
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeImpl.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.Lists.asList;
+import static com.google.common.truth.extensions.proto.FieldScopeUtil.asList;
+import static com.google.common.truth.extensions.proto.FieldScopeUtil.join;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+import com.google.common.base.Optional;
+import com.google.common.collect.Lists;
+import com.google.protobuf.Descriptors.Descriptor;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.Message;
+import java.util.List;
+
+/**
+ * Implementation of a {@link FieldScope}. It takes a logic component {@link FieldScopeLogic}, and
+ * combines it with a human-readable string for use in test failures.
+ *
+ * <p>{@link FieldScopeLogic} objects may be reused, cached, or otherwise implement particular
+ * efficiencies, whereas the human-readable strings are ad-hoc and may be different for otherwise
+ * identical logical implementations. For this reason, and to ensure every {@link FieldScope} gets
+ * an appropriate testing string, we separate the logic components from the public interface.
+ */
+@AutoValue
+abstract class FieldScopeImpl extends FieldScope {
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // AutoValue methods.
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+
+ private static FieldScope create(
+ FieldScopeLogic logic,
+ Function<? super Optional<Descriptor>, String> usingCorrespondenceStringFunction) {
+ return new AutoValue_FieldScopeImpl(logic, usingCorrespondenceStringFunction);
+ }
+
+ @Override
+ abstract FieldScopeLogic logic();
+
+ abstract Function<? super Optional<Descriptor>, String> usingCorrespondenceStringFunction();
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // Instantiation methods.
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+
+ static FieldScope createFromSetFields(Message message) {
+ return create(
+ FieldScopeLogic.partialScope(message),
+ Functions.constant(String.format("FieldScopes.fromSetFields({%s})", message.toString())));
+ }
+
+ static FieldScope createFromSetFields(Iterable<? extends Message> messages) {
+ if (emptyOrAllNull(messages)) {
+ return create(
+ FieldScopeLogic.none(),
+ Functions.constant(String.format("FieldScopes.fromSetFields(%s)", messages.toString())));
+ }
+
+ Optional<Descriptor> optDescriptor = FieldScopeUtil.getSingleDescriptor(messages);
+ checkArgument(
+ optDescriptor.isPresent(),
+ "Cannot create scope from messages with different descriptors: %s",
+ getDescriptors(messages));
+
+ return create(
+ FieldScopeLogic.partialScope(messages, optDescriptor.get()),
+ Functions.constant(String.format("FieldScopes.fromSetFields(%s)", formatList(messages))));
+ }
+
+ static FieldScope createIgnoringFields(Iterable<Integer> fieldNumbers) {
+ return create(
+ FieldScopeLogic.all().ignoringFields(fieldNumbers),
+ FieldScopeUtil.fieldNumbersFunction("FieldScopes.ignoringFields(%s)", fieldNumbers));
+ }
+
+ static FieldScope createIgnoringFieldDescriptors(Iterable<FieldDescriptor> fieldDescriptors) {
+ return create(
+ FieldScopeLogic.all().ignoringFieldDescriptors(fieldDescriptors),
+ Functions.constant(
+ String.format("FieldScopes.ignoringFieldDescriptors(%s)", join(fieldDescriptors))));
+ }
+
+ static FieldScope createAllowingFields(Iterable<Integer> fieldNumbers) {
+ return create(
+ FieldScopeLogic.none().allowingFields(fieldNumbers),
+ FieldScopeUtil.fieldNumbersFunction("FieldScopes.allowingFields(%s)", fieldNumbers));
+ }
+
+ static FieldScope createAllowingFieldDescriptors(Iterable<FieldDescriptor> fieldDescriptors) {
+ return create(
+ FieldScopeLogic.none().allowingFieldDescriptors(fieldDescriptors),
+ Functions.constant(
+ String.format("FieldScopes.allowingFieldDescriptors(%s)", join(fieldDescriptors))));
+ }
+
+ private static final FieldScope ALL =
+ create(FieldScopeLogic.all(), Functions.constant("FieldScopes.all()"));
+ private static final FieldScope NONE =
+ create(FieldScopeLogic.none(), Functions.constant("FieldScopes.none()"));
+
+ static FieldScope all() {
+ return ALL;
+ }
+
+ static FieldScope none() {
+ return NONE;
+ }
+
+ private static boolean emptyOrAllNull(Iterable<?> objects) {
+ for (Object object : objects) {
+ if (object != null) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // Delegation methods.
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+
+ @Override
+ String usingCorrespondenceString(Optional<Descriptor> descriptor) {
+ return usingCorrespondenceStringFunction().apply(descriptor);
+ }
+
+ @Override
+ public final FieldScope ignoringFields(int firstFieldNumber, int... rest) {
+ return ignoringFields(asList(firstFieldNumber, rest));
+ }
+
+ @Override
+ public final FieldScope ignoringFields(Iterable<Integer> fieldNumbers) {
+ return create(
+ logic().ignoringFields(fieldNumbers),
+ addUsingCorrespondenceFieldNumbersString(".ignoringFields(%s)", fieldNumbers));
+ }
+
+ @Override
+ public final FieldScope ignoringFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return ignoringFieldDescriptors(asList(firstFieldDescriptor, rest));
+ }
+
+ @Override
+ public final FieldScope ignoringFieldDescriptors(Iterable<FieldDescriptor> fieldDescriptors) {
+ return create(
+ logic().ignoringFieldDescriptors(fieldDescriptors),
+ addUsingCorrespondenceFieldDescriptorsString(
+ ".ignoringFieldDescriptors(%s)", fieldDescriptors));
+ }
+
+ @Override
+ public final FieldScope allowingFields(int firstFieldNumber, int... rest) {
+ return allowingFields(asList(firstFieldNumber, rest));
+ }
+
+ @Override
+ public final FieldScope allowingFields(Iterable<Integer> fieldNumbers) {
+ return create(
+ logic().allowingFields(fieldNumbers),
+ addUsingCorrespondenceFieldNumbersString(".allowingFields(%s)", fieldNumbers));
+ }
+
+ @Override
+ public final FieldScope allowingFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return allowingFieldDescriptors(asList(firstFieldDescriptor, rest));
+ }
+
+ @Override
+ public final FieldScope allowingFieldDescriptors(Iterable<FieldDescriptor> fieldDescriptors) {
+ return create(
+ logic().allowingFieldDescriptors(fieldDescriptors),
+ addUsingCorrespondenceFieldDescriptorsString(
+ ".allowingFieldDescriptors(%s)", fieldDescriptors));
+ }
+
+ private Function<Optional<Descriptor>, String> addUsingCorrespondenceFieldNumbersString(
+ String fmt, Iterable<Integer> fieldNumbers) {
+ return FieldScopeUtil.concat(
+ usingCorrespondenceStringFunction(),
+ FieldScopeUtil.fieldNumbersFunction(fmt, fieldNumbers));
+ }
+
+ private Function<Optional<Descriptor>, String> addUsingCorrespondenceFieldDescriptorsString(
+ String fmt, Iterable<FieldDescriptor> fieldDescriptors) {
+ return FieldScopeUtil.concat(
+ usingCorrespondenceStringFunction(),
+ Functions.constant(String.format(fmt, join(fieldDescriptors))));
+ }
+
+ private static Iterable<String> getDescriptors(Iterable<? extends Message> messages) {
+ List<String> descriptors = Lists.newArrayList();
+ for (Message message : messages) {
+ descriptors.add(message == null ? "null" : message.getDescriptorForType().getFullName());
+ }
+ return descriptors;
+ }
+
+ private static String formatList(Iterable<? extends Message> messages) {
+ List<String> strings = Lists.newArrayList();
+ for (Message message : messages) {
+ strings.add(message == null ? "null" : "{" + message + "}");
+ }
+ return "[" + join(strings) + "]";
+ }
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeLogic.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeLogic.java
new file mode 100644
index 00000000..31fd0563
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeLogic.java
@@ -0,0 +1,567 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.truth.extensions.proto.FieldScopeUtil.join;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.google.common.base.Verify;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.ForOverride;
+import com.google.protobuf.Descriptors.Descriptor;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.Message;
+import java.util.List;
+
+/**
+ * Implementations of all variations of {@link FieldScope} logic.
+ *
+ * <p>{@code FieldScopeLogic} is the abstract base class which provides common functionality to all
+ * sub-types. There are two classes of sub-types:
+ *
+ * <ul>
+ * <li>Concrete subtypes, which implements specific rules and perform no delegation.
+ * <li>Compound subtypes, which combine one or more {@code FieldScopeLogic}s with specific
+ * operations.
+ * </ul>
+ */
+abstract class FieldScopeLogic implements FieldScopeLogicContainer<FieldScopeLogic> {
+
+ /**
+ * Returns whether the given field is included in this FieldScopeLogic, along with whether it's
+ * included recursively or not.
+ */
+ abstract FieldScopeResult policyFor(Descriptor rootDescriptor, SubScopeId subScopeId);
+
+ /** Returns whether the given field is included in this FieldScopeLogic. */
+ final boolean contains(Descriptor rootDescriptor, SubScopeId subScopeId) {
+ return policyFor(rootDescriptor, subScopeId).included();
+ }
+
+ /**
+ * Returns a {@code FieldScopeLogic} to handle the message pointed to by this descriptor.
+ *
+ * <p>Subclasses which can return non-recursive {@link FieldScopeResult}s must override {@link
+ * #subScopeImpl} to implement those cases.
+ */
+ @Override
+ public final FieldScopeLogic subScope(Descriptor rootDescriptor, SubScopeId subScopeId) {
+ FieldScopeResult result = policyFor(rootDescriptor, subScopeId);
+ if (result.recursive()) {
+ return result.included() ? all() : none();
+ } else {
+ return subScopeImpl(rootDescriptor, subScopeId);
+ }
+ }
+
+ /**
+ * Returns {@link #subScope} for {@code NONRECURSIVE} results.
+ *
+ * <p>Throws an {@link UnsupportedOperationException} by default. Subclasses which can return
+ * {@code NONRECURSIVE} results must override this method.
+ */
+ @ForOverride
+ FieldScopeLogic subScopeImpl(Descriptor rootDescriptor, SubScopeId subScopeId) {
+ throw new UnsupportedOperationException("subScopeImpl not implemented for " + getClass());
+ }
+
+ /**
+ * Returns an accurate description for debugging purposes.
+ *
+ * <p>Compare to {@link FieldScope#usingCorrespondenceString(Optional)}, which returns a beautiful
+ * error message that makes as much sense to the user as possible.
+ *
+ * <p>Abstract so subclasses must implement.
+ */
+ @Override
+ public abstract String toString();
+
+ @Override
+ public void validate(
+ Descriptor rootDescriptor, FieldDescriptorValidator fieldDescriptorValidator) {}
+
+ private static boolean isEmpty(Iterable<?> container) {
+ boolean isEmpty = true;
+ for (Object element : container) {
+ checkNotNull(element);
+ isEmpty = false;
+ }
+
+ return isEmpty;
+ }
+
+ // TODO(user): Rename these 'ignoring' and 'allowing' methods to 'plus' and 'minus', or
+ // something else that doesn't tightly couple FieldScopeLogic to the 'ignore' concept.
+ FieldScopeLogic ignoringFields(Iterable<Integer> fieldNumbers) {
+ if (isEmpty(fieldNumbers)) {
+ return this;
+ }
+ return and(
+ this,
+ new NegationFieldScopeLogic(new FieldNumbersLogic(fieldNumbers, /* isRecursive = */ true)));
+ }
+
+ FieldScopeLogic ignoringFieldDescriptors(Iterable<FieldDescriptor> fieldDescriptors) {
+ if (isEmpty(fieldDescriptors)) {
+ return this;
+ }
+ return and(
+ this,
+ new NegationFieldScopeLogic(
+ new FieldDescriptorsLogic(fieldDescriptors, /* isRecursive = */ true)));
+ }
+
+ FieldScopeLogic allowingFields(Iterable<Integer> fieldNumbers) {
+ if (isEmpty(fieldNumbers)) {
+ return this;
+ }
+ return or(this, new FieldNumbersLogic(fieldNumbers, /* isRecursive = */ true));
+ }
+
+ FieldScopeLogic allowingFieldsNonRecursive(Iterable<Integer> fieldNumbers) {
+ if (isEmpty(fieldNumbers)) {
+ return this;
+ }
+ return or(this, new FieldNumbersLogic(fieldNumbers, /* isRecursive = */ false));
+ }
+
+ FieldScopeLogic allowingFieldDescriptors(Iterable<FieldDescriptor> fieldDescriptors) {
+ if (isEmpty(fieldDescriptors)) {
+ return this;
+ }
+ return or(this, new FieldDescriptorsLogic(fieldDescriptors, /* isRecursive = */ true));
+ }
+
+ FieldScopeLogic allowingFieldDescriptorsNonRecursive(Iterable<FieldDescriptor> fieldDescriptors) {
+ if (isEmpty(fieldDescriptors)) {
+ return this;
+ }
+ return or(this, new FieldDescriptorsLogic(fieldDescriptors, /* isRecursive = */ false));
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // CONCRETE SUBTYPES
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+
+ /** Returns whether this is equivalent to {@code FieldScopeLogic.all()}. */
+ boolean isAll() {
+ return false;
+ }
+
+ private static final FieldScopeLogic ALL =
+ new FieldScopeLogic() {
+ @Override
+ public String toString() {
+ return "FieldScopes.all()";
+ }
+
+ @Override
+ final FieldScopeResult policyFor(Descriptor rootDescriptor, SubScopeId subScopeId) {
+ return FieldScopeResult.INCLUDED_RECURSIVELY;
+ }
+
+ @Override
+ final boolean isAll() {
+ return true;
+ }
+ };
+
+ private static final FieldScopeLogic NONE =
+ new FieldScopeLogic() {
+ @Override
+ public String toString() {
+ return "FieldScopes.none()";
+ }
+
+ @Override
+ final FieldScopeResult policyFor(Descriptor rootDescriptor, SubScopeId subScopeId) {
+ return FieldScopeResult.EXCLUDED_RECURSIVELY;
+ }
+ };
+
+ static FieldScopeLogic all() {
+ return ALL;
+ }
+
+ static FieldScopeLogic none() {
+ return NONE;
+ }
+
+ private static class PartialScopeLogic extends FieldScopeLogic {
+ private static final PartialScopeLogic EMPTY = new PartialScopeLogic(FieldNumberTree.empty());
+
+ private final FieldNumberTree fieldNumberTree;
+
+ PartialScopeLogic(FieldNumberTree fieldNumberTree) {
+ this.fieldNumberTree = fieldNumberTree;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("PartialScopeLogic(%s)", fieldNumberTree);
+ }
+
+ @Override
+ final FieldScopeResult policyFor(Descriptor rootDescriptor, SubScopeId subScopeId) {
+ return fieldNumberTree.hasChild(subScopeId)
+ ? FieldScopeResult.INCLUDED_NONRECURSIVELY
+ : FieldScopeResult.EXCLUDED_RECURSIVELY;
+ }
+
+ @Override
+ final FieldScopeLogic subScopeImpl(Descriptor rootDescriptor, SubScopeId subScopeId) {
+ return newPartialScopeLogic(fieldNumberTree.child(subScopeId));
+ }
+
+ private static PartialScopeLogic newPartialScopeLogic(FieldNumberTree fieldNumberTree) {
+ return fieldNumberTree.isEmpty() ? EMPTY : new PartialScopeLogic(fieldNumberTree);
+ }
+ }
+
+ private static final class RootPartialScopeLogic extends PartialScopeLogic {
+ private final String repr;
+ private final Descriptor expectedDescriptor;
+
+ RootPartialScopeLogic(FieldNumberTree fieldNumberTree, String repr, Descriptor descriptor) {
+ super(fieldNumberTree);
+ this.repr = repr;
+ this.expectedDescriptor = descriptor;
+ }
+
+ @Override
+ public void validate(
+ Descriptor rootDescriptor, FieldDescriptorValidator fieldDescriptorValidator) {
+ Verify.verify(
+ fieldDescriptorValidator == FieldDescriptorValidator.ALLOW_ALL,
+ "PartialScopeLogic doesn't support custom field validators.");
+
+ checkArgument(
+ expectedDescriptor.equals(rootDescriptor),
+ "Message given to FieldScopes.fromSetFields() does not have the same descriptor as the "
+ + "message being tested. Expected %s, got %s.",
+ expectedDescriptor.getFullName(),
+ rootDescriptor.getFullName());
+ }
+
+ @Override
+ public String toString() {
+ return String.format("FieldScopes.fromSetFields(%s)", repr);
+ }
+ }
+
+ static FieldScopeLogic partialScope(Message message) {
+ return new RootPartialScopeLogic(
+ FieldNumberTree.fromMessage(message), message.toString(), message.getDescriptorForType());
+ }
+
+ static FieldScopeLogic partialScope(Iterable<? extends Message> messages, Descriptor descriptor) {
+ return new RootPartialScopeLogic(
+ FieldNumberTree.fromMessages(messages),
+ Joiner.on(", ").useForNull("null").join(messages),
+ descriptor);
+ }
+
+ // TODO(user): Performance: Optimize FieldNumbersLogic and FieldDescriptorsLogic for
+ // adding / ignoring field numbers and descriptors, respectively, to eliminate high recursion
+ // costs for long chains of allows/ignores.
+
+ // Common functionality for FieldNumbersLogic and FieldDescriptorsLogic.
+ private abstract static class FieldMatcherLogicBase extends FieldScopeLogic {
+
+ private final boolean isRecursive;
+
+ protected FieldMatcherLogicBase(boolean isRecursive) {
+ this.isRecursive = isRecursive;
+ }
+
+ /**
+ * Determines whether the FieldDescriptor is equal to one of the explicitly defined components
+ * of this FieldScopeLogic.
+ *
+ * @param descriptor Descriptor of the message being tested.
+ * @param fieldDescriptor FieldDescriptor being inspected for a direct match to the scope's
+ * definition.
+ */
+ abstract boolean matchesFieldDescriptor(Descriptor descriptor, FieldDescriptor fieldDescriptor);
+
+ @Override
+ final FieldScopeResult policyFor(Descriptor rootDescriptor, SubScopeId subScopeId) {
+ if (subScopeId.kind() == SubScopeId.Kind.UNKNOWN_FIELD_DESCRIPTOR) {
+ return FieldScopeResult.EXCLUDED_RECURSIVELY;
+ }
+
+ FieldDescriptor fieldDescriptor = subScopeId.fieldDescriptor();
+ if (matchesFieldDescriptor(rootDescriptor, fieldDescriptor)) {
+ return FieldScopeResult.of(/* included = */ true, isRecursive);
+ }
+
+ // We return 'EXCLUDED_NONRECURSIVELY' for both field descriptor scopes and field number
+ // scopes. In the former case, the field descriptors are arbitrary, so it's always possible we
+ // find a hit on a sub-message somewhere. In the latter case, the message definition may be
+ // cyclic, so we need to return 'EXCLUDED_NONRECURSIVELY' even if the top level field number
+ // doesn't match.
+ return FieldScopeResult.EXCLUDED_NONRECURSIVELY;
+ }
+
+ @Override
+ final FieldScopeLogic subScopeImpl(Descriptor rootDescriptor, SubScopeId subScopeId) {
+ return this;
+ }
+
+ @Override
+ public void validate(
+ Descriptor rootDescriptor, FieldDescriptorValidator fieldDescriptorValidator) {
+ if (isRecursive) {
+ Verify.verify(
+ fieldDescriptorValidator == FieldDescriptorValidator.ALLOW_ALL,
+ "Field descriptor validators are not supported "
+ + "for non-recursive field matcher logics.");
+ }
+ }
+ }
+
+ // Matches any specific fields which fall under a sub-message field (or root) matching the root
+ // message type and one of the specified field numbers.
+ private static final class FieldNumbersLogic extends FieldMatcherLogicBase {
+ private final ImmutableSet<Integer> fieldNumbers;
+
+ FieldNumbersLogic(Iterable<Integer> fieldNumbers, boolean isRecursive) {
+ super(isRecursive);
+ this.fieldNumbers = ImmutableSet.copyOf(fieldNumbers);
+ }
+
+ @Override
+ public void validate(
+ Descriptor rootDescriptor, FieldDescriptorValidator fieldDescriptorValidator) {
+ super.validate(rootDescriptor, fieldDescriptorValidator);
+ for (int fieldNumber : fieldNumbers) {
+ FieldDescriptor fieldDescriptor = rootDescriptor.findFieldByNumber(fieldNumber);
+ checkArgument(
+ fieldDescriptor != null,
+ "Message type %s has no field with number %s.",
+ rootDescriptor.getFullName(),
+ fieldNumber);
+ fieldDescriptorValidator.validate(fieldDescriptor);
+ }
+ }
+
+ @Override
+ boolean matchesFieldDescriptor(Descriptor descriptor, FieldDescriptor fieldDescriptor) {
+ return fieldDescriptor.getContainingType() == descriptor
+ && fieldNumbers.contains(fieldDescriptor.getNumber());
+ }
+
+ @Override
+ public String toString() {
+ return String.format("FieldScopes.allowingFields(%s)", join(fieldNumbers));
+ }
+ }
+
+ // Matches any specific fields which fall under one of the specified FieldDescriptors.
+ private static final class FieldDescriptorsLogic extends FieldMatcherLogicBase {
+ private final ImmutableSet<FieldDescriptor> fieldDescriptors;
+
+ FieldDescriptorsLogic(Iterable<FieldDescriptor> fieldDescriptors, boolean isRecursive) {
+ super(isRecursive);
+ this.fieldDescriptors = ImmutableSet.copyOf(fieldDescriptors);
+ }
+
+ @Override
+ boolean matchesFieldDescriptor(Descriptor descriptor, FieldDescriptor fieldDescriptor) {
+ return fieldDescriptors.contains(fieldDescriptor);
+ }
+
+ @Override
+ public void validate(
+ Descriptor rootDescriptor, FieldDescriptorValidator fieldDescriptorValidator) {
+ super.validate(rootDescriptor, fieldDescriptorValidator);
+ for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
+ fieldDescriptorValidator.validate(fieldDescriptor);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format("FieldScopes.allowingFieldDescriptors(%s)", join(fieldDescriptors));
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // COMPOUND SUBTYPES
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+
+ private abstract static class CompoundFieldScopeLogic<T extends CompoundFieldScopeLogic<T>>
+ extends FieldScopeLogic {
+ final ImmutableList<FieldScopeLogic> elements;
+
+ CompoundFieldScopeLogic(FieldScopeLogic singleElem) {
+ this.elements = ImmutableList.of(singleElem);
+ }
+
+ CompoundFieldScopeLogic(FieldScopeLogic firstElem, FieldScopeLogic secondElem) {
+ this.elements = ImmutableList.of(firstElem, secondElem);
+ }
+
+ @Override
+ public final void validate(
+ Descriptor rootDescriptor, FieldDescriptorValidator fieldDescriptorValidator) {
+ for (FieldScopeLogic elem : elements) {
+ elem.validate(rootDescriptor, fieldDescriptorValidator);
+ }
+ }
+
+ /** Helper to produce a new {@code CompoundFieldScopeLogic} of the same type as the subclass. */
+ abstract T newLogicOfSameType(List<FieldScopeLogic> newElements);
+
+ @Override
+ final FieldScopeLogic subScopeImpl(Descriptor rootDescriptor, SubScopeId subScopeId) {
+ ImmutableList.Builder<FieldScopeLogic> builder =
+ ImmutableList.builderWithExpectedSize(elements.size());
+ for (FieldScopeLogic elem : elements) {
+ builder.add(elem.subScope(rootDescriptor, subScopeId));
+ }
+ return newLogicOfSameType(builder.build());
+ }
+ }
+
+ private static final class IntersectionFieldScopeLogic
+ extends CompoundFieldScopeLogic<IntersectionFieldScopeLogic> {
+ IntersectionFieldScopeLogic(FieldScopeLogic subject1, FieldScopeLogic subject2) {
+ super(subject1, subject2);
+ }
+
+ @Override
+ IntersectionFieldScopeLogic newLogicOfSameType(List<FieldScopeLogic> newElements) {
+ checkArgument(newElements.size() == 2, "Expected 2 elements: %s", newElements);
+ return new IntersectionFieldScopeLogic(newElements.get(0), newElements.get(1));
+ }
+
+ @Override
+ FieldScopeResult policyFor(Descriptor rootDescriptor, SubScopeId subScopeId) {
+ // The intersection of two scopes is ignorable if either scope is itself ignorable.
+ return intersection(
+ elements.get(0).policyFor(rootDescriptor, subScopeId),
+ elements.get(1).policyFor(rootDescriptor, subScopeId));
+ }
+
+ private static FieldScopeResult intersection(
+ FieldScopeResult result1, FieldScopeResult result2) {
+ if (result1 == FieldScopeResult.EXCLUDED_RECURSIVELY
+ || result2 == FieldScopeResult.EXCLUDED_RECURSIVELY) {
+ // If either argument is excluded recursively, the result is too.
+ return FieldScopeResult.EXCLUDED_RECURSIVELY;
+ } else if (!result1.included() || !result2.included()) {
+ // Otherwise, we exclude non-recursively if either result is an exclusion.
+ return FieldScopeResult.EXCLUDED_NONRECURSIVELY;
+ } else if (result1.recursive() && result2.recursive()) {
+ // We include recursively if both arguments are recursive.
+ return FieldScopeResult.INCLUDED_RECURSIVELY;
+ } else {
+ // Otherwise, we include non-recursively.
+ return FieldScopeResult.INCLUDED_NONRECURSIVELY;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format("(%s && %s)", elements.get(0), elements.get(1));
+ }
+ }
+
+ private static final class UnionFieldScopeLogic
+ extends CompoundFieldScopeLogic<UnionFieldScopeLogic> {
+ UnionFieldScopeLogic(FieldScopeLogic subject1, FieldScopeLogic subject2) {
+ super(subject1, subject2);
+ }
+
+ @Override
+ UnionFieldScopeLogic newLogicOfSameType(List<FieldScopeLogic> newElements) {
+ checkArgument(newElements.size() == 2, "Expected 2 elements: %s", newElements);
+ return new UnionFieldScopeLogic(newElements.get(0), newElements.get(1));
+ }
+
+ @Override
+ FieldScopeResult policyFor(Descriptor rootDescriptor, SubScopeId subScopeId) {
+ // The union of two scopes is ignorable only if both scopes are themselves ignorable.
+ return union(
+ elements.get(0).policyFor(rootDescriptor, subScopeId),
+ elements.get(1).policyFor(rootDescriptor, subScopeId));
+ }
+
+ private static FieldScopeResult union(FieldScopeResult result1, FieldScopeResult result2) {
+ if (result1 == FieldScopeResult.INCLUDED_RECURSIVELY
+ || result2 == FieldScopeResult.INCLUDED_RECURSIVELY) {
+ // If either argument is included recursively, the result is too.
+ return FieldScopeResult.INCLUDED_RECURSIVELY;
+ } else if (result1.included() || result2.included()) {
+ // Otherwise, if either is included, we include non-recursively.
+ return FieldScopeResult.INCLUDED_NONRECURSIVELY;
+ } else if (result1.recursive() && result2.recursive()) {
+ // If both arguments are recursive, we exclude recursively.
+ return FieldScopeResult.EXCLUDED_RECURSIVELY;
+ } else {
+ // Otherwise, we exclude exclude non-recursively.
+ return FieldScopeResult.EXCLUDED_NONRECURSIVELY;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format("(%s || %s)", elements.get(0), elements.get(1));
+ }
+ }
+
+ private static final class NegationFieldScopeLogic
+ extends CompoundFieldScopeLogic<NegationFieldScopeLogic> {
+ NegationFieldScopeLogic(FieldScopeLogic subject) {
+ super(subject);
+ }
+
+ @Override
+ NegationFieldScopeLogic newLogicOfSameType(List<FieldScopeLogic> newElements) {
+ checkArgument(newElements.size() == 1, "Expected 1 element: %s", newElements);
+ return new NegationFieldScopeLogic(newElements.get(0));
+ }
+
+ @Override
+ FieldScopeResult policyFor(Descriptor rootDescriptor, SubScopeId subScopeId) {
+ FieldScopeResult result = elements.get(0).policyFor(rootDescriptor, subScopeId);
+ return FieldScopeResult.of(!result.included(), result.recursive());
+ }
+
+ @Override
+ public String toString() {
+ return String.format("!(%s)", elements.get(0));
+ }
+ }
+
+ static FieldScopeLogic and(FieldScopeLogic fieldScopeLogic1, FieldScopeLogic fieldScopeLogic2) {
+ return new IntersectionFieldScopeLogic(fieldScopeLogic1, fieldScopeLogic2);
+ }
+
+ static FieldScopeLogic or(FieldScopeLogic fieldScopeLogic1, FieldScopeLogic fieldScopeLogic2) {
+ return new UnionFieldScopeLogic(fieldScopeLogic1, fieldScopeLogic2);
+ }
+
+ static FieldScopeLogic not(FieldScopeLogic fieldScopeLogic) {
+ return new NegationFieldScopeLogic(fieldScopeLogic);
+ }
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeLogicContainer.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeLogicContainer.java
new file mode 100644
index 00000000..e3d3ea4a
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeLogicContainer.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2018 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import com.google.protobuf.Descriptors.Descriptor;
+
+/**
+ * Critical methods for anything which is, or contains a {@link FieldScopeLogic}.
+ *
+ * <p>All such containers must support invoking {@code subScope} for scoping to sub-messages, and
+ * all must support validation of integer field numbers.
+ */
+interface FieldScopeLogicContainer<T extends FieldScopeLogicContainer<T>> {
+
+ /** Returns the analog of {@link FieldScopeLogic#subScope} for this container. */
+ T subScope(Descriptor rootDescriptor, SubScopeId subScopeId);
+
+ /** Validates explicitly specified fields for this container. */
+ void validate(Descriptor rootDescriptor, FieldDescriptorValidator fieldDescriptorValidator);
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeLogicMap.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeLogicMap.java
new file mode 100644
index 00000000..4bd0948d
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeLogicMap.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2018 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.Descriptors.Descriptor;
+
+/**
+ * An immutable map of {@link FieldScopeLogic} to an arbitrary value type.
+ *
+ * <p>This is the field-path based analog to {@link com.google.common.collect.RangeMap}. Setting a
+ * value {@code v} for a {@link FieldScopeLogic} {@code l} on the {@code FieldScopeLogicMap} means
+ * that, for any field path {@code f} contained in {@code l}, the value mapped to {@code f} is
+ * {@code v}. This overrides any previous {FieldScopeLogic -> value} pairs/mappings.
+ *
+ * <p>Unlike {@link com.google.common.collect.RangeMap}, this class does not support analysis of its
+ * internals, only {@link #get} operations, and it is not performant. All {@code get} and {@code
+ * with} operations are O(N), where N = number of mappings, so this class is intended only for small
+ * numbers of entries.
+ */
+class FieldScopeLogicMap<V> implements FieldScopeLogicContainer<FieldScopeLogicMap<V>> {
+
+ @AutoValue
+ abstract static class Entry<V> {
+ abstract FieldScopeLogic fieldScopeLogic();
+
+ abstract V value();
+
+ static <V> Entry<V> of(FieldScopeLogic fieldScopeLogic, V value) {
+ return new AutoValue_FieldScopeLogicMap_Entry<>(fieldScopeLogic, value);
+ }
+ }
+
+ private static final FieldScopeLogicMap<Object> EMPTY_INSTANCE =
+ new FieldScopeLogicMap<>(ImmutableList.<Entry<Object>>of());
+
+ // Key -> value mappings for this map. Earlier entries override later ones.
+ private final ImmutableList<Entry<V>> entries;
+
+ private FieldScopeLogicMap(Iterable<Entry<V>> entries) {
+ this.entries = ImmutableList.copyOf(entries);
+ }
+
+ public boolean isEmpty() {
+ return entries.isEmpty();
+ }
+
+ public Optional<V> get(Descriptor rootDescriptor, SubScopeId subScopeId) {
+ // Earlier entries override later ones, so we don't need to iterate backwards.
+ for (Entry<V> entry : entries) {
+ if (entry.fieldScopeLogic().contains(rootDescriptor, subScopeId)) {
+ return Optional.of(entry.value());
+ }
+ }
+ return Optional.absent();
+ }
+
+ /** Returns a new immutable map that adds the given fields -> value mapping. */
+ public FieldScopeLogicMap<V> with(FieldScopeLogic fieldScopeLogic, V value) {
+ ImmutableList.Builder<Entry<V>> newEntries = ImmutableList.builder();
+ // Earlier entries override later ones, so we insert the new one at the front of the list.
+ newEntries.add(Entry.of(fieldScopeLogic, value));
+ newEntries.addAll(entries);
+ return new FieldScopeLogicMap<>(newEntries.build());
+ }
+
+ @Override
+ public FieldScopeLogicMap<V> subScope(Descriptor rootDescriptor, SubScopeId subScopeId) {
+ ImmutableList.Builder<Entry<V>> newEntries =
+ ImmutableList.builderWithExpectedSize(entries.size());
+ for (Entry<V> entry : entries) {
+ newEntries.add(
+ Entry.of(entry.fieldScopeLogic().subScope(rootDescriptor, subScopeId), entry.value()));
+ }
+ return new FieldScopeLogicMap<>(newEntries.build());
+ }
+
+ @Override
+ public void validate(
+ Descriptor rootDescriptor, FieldDescriptorValidator fieldDescriptorValidator) {
+ for (Entry<V> entry : entries) {
+ entry.fieldScopeLogic().validate(rootDescriptor, fieldDescriptorValidator);
+ }
+ }
+
+ @SuppressWarnings("unchecked") // Implementation is fully variant.
+ public static <V> FieldScopeLogicMap<V> empty() {
+ return (FieldScopeLogicMap<V>) EMPTY_INSTANCE;
+ }
+
+ /** Returns a map which maps all fields to the given value by default. */
+ public static <V> FieldScopeLogicMap<V> defaultValue(V value) {
+ return new FieldScopeLogicMap<>(ImmutableList.of(Entry.of(FieldScopeLogic.all(), value)));
+ }
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeResult.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeResult.java
new file mode 100644
index 00000000..1f14afdb
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeResult.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2018 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+/**
+ * Whether or not a sub-message tree is considered part of the enclosing scope.
+ *
+ * <p>This enables {@link FieldScopeLogic}s and the {@code ProtoTruthMessageDifferencer} to work
+ * together on traversing a message, instead of either class doing redundant work. The need for
+ * {@code NONRECURSIVE} arises from sub-messages. For example:
+ *
+ * <p><code>
+ * message Foo {
+ * optional Bar bar = 1;
+ * }
+ *
+ * message Bar {
+ * optional Baz baz = 1;
+ * }
+ *
+ * message Baz {
+ * optional string name = 1;
+ * optional int64 id = 2;
+ * }
+ * </code>
+ *
+ * <p>A {@link FieldScopeLogic} which excludes everything except 'Baz.name', when asked if 'Foo.bar'
+ * should be ignored, cannot know whether it should be excluded or not without scanning all of
+ * 'Foo.bar' for Baz submessages, and whether they have the name field set. We could scan the entire
+ * message to make this decision, but the message differencer will be scanning anyway if we choose
+ * not to excluded it, which creates redundant work. {@code NONRECURSIVE} is the solution to this
+ * problem: The logic defers the decision back to the message differencer, which proceeds with the
+ * complete scan of 'Foo.bar', and excludes the entire submessage if and only if nothing in
+ * 'Foo.bar' was determined to be un-excludable.
+ */
+enum FieldScopeResult {
+ /** This field is included in this scope, but children might be excludable. */
+ INCLUDED_NONRECURSIVELY(true, false),
+ /** This field and all its children are included in the scope. */
+ INCLUDED_RECURSIVELY(true, true),
+ /** This field is excluded from the scope, but children might be includable. */
+ EXCLUDED_NONRECURSIVELY(false, false),
+ /** This field and all its children are excluded from the scope. */
+ EXCLUDED_RECURSIVELY(false, true);
+
+ public static FieldScopeResult of(boolean included, boolean recursively) {
+ if (included) {
+ return recursively ? INCLUDED_RECURSIVELY : INCLUDED_NONRECURSIVELY;
+ } else {
+ return recursively ? EXCLUDED_RECURSIVELY : EXCLUDED_NONRECURSIVELY;
+ }
+ }
+
+ private final boolean included;
+ private final boolean recursive;
+
+ FieldScopeResult(boolean included, boolean recursive) {
+ this.included = included;
+ this.recursive = recursive;
+ }
+
+ /** Whether this field should be included or not. */
+ boolean included() {
+ return included;
+ }
+
+ /**
+ * Whether this field's sub-children should also be unilaterally included or excluded, conditional
+ * on {@link #included()}
+ */
+ boolean recursive() {
+ return recursive;
+ }
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeUtil.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeUtil.java
new file mode 100644
index 00000000..ae0b634c
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeUtil.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.google.common.collect.Lists;
+import com.google.common.primitives.Ints;
+import com.google.protobuf.Descriptors.Descriptor;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.Message;
+import java.util.List;
+
+/** Utility methods for {@link FieldScope}s and {@link FluentEqualityConfig}. */
+final class FieldScopeUtil {
+ /**
+ * Returns a function which translates integer field numbers into field names using the Descriptor
+ * if available.
+ *
+ * @param fmt Format string that must contain exactly one '%s' and no other format parameters.
+ */
+ static Function<Optional<Descriptor>, String> fieldNumbersFunction(
+ final String fmt, final Iterable<Integer> fieldNumbers) {
+ return new Function<Optional<Descriptor>, String>() {
+ @Override
+ public String apply(Optional<Descriptor> optDescriptor) {
+ return resolveFieldNumbers(optDescriptor, fmt, fieldNumbers);
+ }
+ };
+ }
+
+ /**
+ * Returns a function which formats the given string by getting the usingCorrespondenceString from
+ * the given FieldScope with the argument descriptor.
+ *
+ * @param fmt Format string that must contain exactly one '%s' and no other format parameters.
+ */
+ static Function<Optional<Descriptor>, String> fieldScopeFunction(
+ final String fmt, final FieldScope fieldScope) {
+ return new Function<Optional<Descriptor>, String>() {
+ @Override
+ public String apply(Optional<Descriptor> optDescriptor) {
+ return String.format(fmt, fieldScope.usingCorrespondenceString(optDescriptor));
+ }
+ };
+ }
+
+ /** Returns a function which concatenates the outputs of the two input functions. */
+ static Function<Optional<Descriptor>, String> concat(
+ final Function<? super Optional<Descriptor>, String> function1,
+ final Function<? super Optional<Descriptor>, String> function2) {
+ return new Function<Optional<Descriptor>, String>() {
+ @Override
+ public String apply(Optional<Descriptor> optDescriptor) {
+ return function1.apply(optDescriptor) + function2.apply(optDescriptor);
+ }
+ };
+ }
+
+ /**
+ * Returns the singular descriptor used by all non-null messages in the list.
+ *
+ * <p>If there is no descriptor, or more than one, returns {@code Optional.absent()}.
+ */
+ static Optional<Descriptor> getSingleDescriptor(Iterable<? extends Message> messages) {
+ Optional<Descriptor> optDescriptor = Optional.absent();
+ for (Message message : messages) {
+ if (message != null) {
+ Descriptor descriptor = message.getDescriptorForType();
+ if (!optDescriptor.isPresent()) {
+ optDescriptor = Optional.of(descriptor);
+ } else if (descriptor != optDescriptor.get()) {
+ // Two different descriptors - abandon ship.
+ return Optional.absent();
+ }
+ }
+ }
+ return optDescriptor;
+ }
+
+ /** Joins the arguments into a {@link List} for convenience. */
+ static List<Integer> asList(int first, int... rest) {
+ List<Integer> list = Lists.newArrayList();
+ list.add(first);
+ list.addAll(Ints.asList(rest));
+ return list;
+ }
+
+ private static final Joiner JOINER = Joiner.on(", ");
+
+ static String join(Iterable<?> objects) {
+ return JOINER.join(objects);
+ }
+
+ /**
+ * Formats {@code fmt} with the field numbers, concatenated, if a descriptor is available to
+ * resolve them to field names. Otherwise it uses the raw integers.
+ *
+ * @param fmt Format string that must contain exactly one '%s' and no other format parameters.
+ */
+ private static String resolveFieldNumbers(
+ Optional<Descriptor> optDescriptor, String fmt, Iterable<Integer> fieldNumbers) {
+ if (optDescriptor.isPresent()) {
+ Descriptor descriptor = optDescriptor.get();
+ List<String> strings = Lists.newArrayList();
+ for (int fieldNumber : fieldNumbers) {
+ FieldDescriptor field = descriptor.findFieldByNumber(fieldNumber);
+ strings.add(field != null ? field.toString() : String.format("%d (?)", fieldNumber));
+ }
+ return String.format(fmt, join(strings));
+ } else {
+ return String.format(fmt, join(fieldNumbers));
+ }
+ }
+
+ private FieldScopeUtil() {}
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopes.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopes.java
new file mode 100644
index 00000000..9b709e55
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopes.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import static com.google.common.collect.Lists.asList;
+import static com.google.common.truth.extensions.proto.FieldScopeUtil.asList;
+
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.Message;
+
+/** Factory class for {@link FieldScope} instances. */
+public final class FieldScopes {
+ /**
+ * Returns a {@link FieldScope} which is constrained to precisely those specific field paths that
+ * are explicitly set in the message. Note that, for version 3 protobufs, such a {@link
+ * FieldScope} will omit fields in the provided message which are set to default values.
+ *
+ * <p>This can be used limit the scope of a comparison to a complex set of fields in a very brief
+ * statement. Often, {@code message} is the expected half of a comparison about to be performed.
+ *
+ * <p>Example usage:
+ *
+ * <pre>{@code
+ * Foo actual = Foo.newBuilder().setBar(3).setBaz(4).build();
+ * Foo expected = Foo.newBuilder().setBar(3).setBaz(5).build();
+ * // Fails, because actual.getBaz() != expected.getBaz().
+ * assertThat(actual).isEqualTo(expected);
+ *
+ * Foo scope = Foo.newBuilder().setBar(2).build();
+ * // Succeeds, because only the field 'bar' is compared.
+ * assertThat(actual).withPartialScope(FieldScopes.fromSetFields(scope)).isEqualTo(expected);
+ *
+ * }</pre>
+ *
+ * <p>The returned {@link FieldScope} does not respect repeated field indices nor map keys. For
+ * example, if the provided message sets different field values for different elements of a
+ * repeated field, like so:
+ *
+ * <pre>{@code
+ * sub_message: {
+ * foo: "foo"
+ * }
+ * sub_message: {
+ * bar: "bar"
+ * }
+ * }</pre>
+ *
+ * <p>The {@link FieldScope} will contain {@code sub_message.foo} and {@code sub_message.bar} for
+ * *all* repeated {@code sub_messages}, including those beyond index 1.
+ */
+ // TODO(user): Figure out a way to improve this without reinventing MessageDifferencer.
+ // Alternatively, gather evidence to show that the existing behavior is fine/preferable.
+ // Alternatively II, add Scope.PARTIAL support to ProtoFluentEquals, but with a different name and
+ // explicit documentation that it may cause issues with Proto 3.
+ public static FieldScope fromSetFields(Message message) {
+ return FieldScopeImpl.createFromSetFields(message);
+ }
+
+ /**
+ * Creates a {@link FieldScope} covering the fields set in every message in the provided list of
+ * messages, with the same semantics as in {@link #fromSetFields(Message)}.
+ *
+ * <p>This can be thought of as the union of the {@link FieldScope}s for each individual message,
+ * or the {@link FieldScope} for the merge of all the messages. These are equivalent.
+ */
+ public static FieldScope fromSetFields(
+ Message firstMessage, Message secondMessage, Message... rest) {
+ return fromSetFields(asList(firstMessage, secondMessage, rest));
+ }
+
+ /**
+ * Creates a {@link FieldScope} covering the fields set in every message in the provided list of
+ * messages, with the same semantics as in {@link #fromSetFields(Message)}.
+ *
+ * <p>This can be thought of as the union of the {@link FieldScope}s for each individual message,
+ * or the {@link FieldScope} for the merge of all the messages. These are equivalent.
+ */
+ public static FieldScope fromSetFields(Iterable<? extends Message> messages) {
+ return FieldScopeImpl.createFromSetFields(messages);
+ }
+
+ /**
+ * Returns a {@link FieldScope} which matches everything except the provided field numbers for the
+ * top level message type.
+ *
+ * <p>The field numbers are ignored recursively on this type. That is, if {@code YourMessage}
+ * contains another {@code YourMessage} somewhere within its subtree, field number {@code X} will
+ * be ignored for all submessages of type {@code YourMessage}, as well as for the top-level
+ * message.
+ *
+ * @see FieldScope#ignoringFields(int, int...)
+ */
+ public static FieldScope ignoringFields(int firstFieldNumber, int... rest) {
+ return FieldScopeImpl.createIgnoringFields(asList(firstFieldNumber, rest));
+ }
+
+ /**
+ * Returns a {@link FieldScope} which matches everything except the provided field numbers for the
+ * top level message type.
+ *
+ * <p>The field numbers are ignored recursively on this type. That is, if {@code YourMessage}
+ * contains another {@code YourMessage} somewhere within its subtree, field number {@code X} will
+ * be ignored for all submessages of type {@code YourMessage}, as well as for the top-level
+ * message.
+ *
+ * @see FieldScope#ignoringFields(Iterable)
+ */
+ public static FieldScope ignoringFields(Iterable<Integer> fieldNumbers) {
+ return FieldScopeImpl.createIgnoringFields(fieldNumbers);
+ }
+
+ /**
+ * Returns a {@link FieldScope} which matches everything except the provided field descriptors for
+ * the message.
+ *
+ * @see FieldScope#ignoringFieldDescriptors(FieldDescriptor, FieldDescriptor...)
+ */
+ public static FieldScope ignoringFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return FieldScopeImpl.createIgnoringFieldDescriptors(asList(firstFieldDescriptor, rest));
+ }
+
+ /**
+ * Returns a {@link FieldScope} which matches everything except the provided field descriptors for
+ * the message.
+ *
+ * @see FieldScope#ignoringFieldDescriptors(Iterable)
+ */
+ public static FieldScope ignoringFieldDescriptors(Iterable<FieldDescriptor> fieldDescriptors) {
+ return FieldScopeImpl.createIgnoringFieldDescriptors(fieldDescriptors);
+ }
+
+ /**
+ * Returns a {@link FieldScope} which matches nothing except the provided field numbers for the
+ * top level message type.
+ *
+ * @see FieldScope#allowingFields(int, int...)
+ */
+ public static FieldScope allowingFields(int firstFieldNumber, int... rest) {
+ return FieldScopeImpl.createAllowingFields(asList(firstFieldNumber, rest));
+ }
+
+ /**
+ * Returns a {@link FieldScope} which matches nothing except the provided field numbers for the
+ * top level message type.
+ *
+ * @see FieldScope#allowingFields(Iterable)
+ */
+ public static FieldScope allowingFields(Iterable<Integer> fieldNumbers) {
+ return FieldScopeImpl.createAllowingFields(fieldNumbers);
+ }
+
+ /**
+ * Returns a {@link FieldScope} which matches nothing except the provided field descriptors for
+ * the message.
+ *
+ * @see FieldScope#allowingFieldDescriptors(FieldDescriptor, FieldDescriptor...)
+ */
+ public static FieldScope allowingFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return FieldScopeImpl.createAllowingFieldDescriptors(asList(firstFieldDescriptor, rest));
+ }
+
+ /**
+ * Returns a {@link FieldScope} which matches nothing except the provided field descriptors for
+ * the message.
+ *
+ * @see FieldScope#allowingFieldDescriptors(Iterable)
+ */
+ public static FieldScope allowingFieldDescriptors(Iterable<FieldDescriptor> fieldDescriptors) {
+ return FieldScopeImpl.createAllowingFieldDescriptors(fieldDescriptors);
+ }
+
+ /**
+ * Returns a {@link FieldScope} which matches all fields without exception. Generally not needed,
+ * since the other factory functions will build on top of this for you.
+ */
+ public static FieldScope all() {
+ return FieldScopeImpl.all();
+ }
+
+ /**
+ * Returns a {@link FieldScope} which matches no fields. A comparison made using this scope alone
+ * will always trivially pass. Generally not needed, since the other factory functions will build
+ * on top of this for you.
+ */
+ public static FieldScope none() {
+ return FieldScopeImpl.none();
+ }
+
+ private FieldScopes() {}
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FluentEqualityConfig.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FluentEqualityConfig.java
new file mode 100644
index 00000000..1d5fff1d
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FluentEqualityConfig.java
@@ -0,0 +1,486 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.truth.extensions.proto.FieldScopeUtil.join;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+import com.google.common.base.Optional;
+import com.google.common.base.Verify;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.truth.Correspondence;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.CheckReturnValue;
+import com.google.protobuf.Descriptors.Descriptor;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.ExtensionRegistry;
+import com.google.protobuf.Message;
+import com.google.protobuf.TypeRegistry;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A specification for a {@link ProtoTruthMessageDifferencer} for comparing two individual
+ * protobufs.
+ *
+ * <p>Can be used to compare lists, maps, and multimaps of protos as well by conversion to a {@link
+ * Correspondence}.
+ */
+@AutoValue
+abstract class FluentEqualityConfig implements FieldScopeLogicContainer<FluentEqualityConfig> {
+
+ private static final FluentEqualityConfig DEFAULT_INSTANCE =
+ new AutoValue_FluentEqualityConfig.Builder()
+ .setIgnoreFieldAbsenceScope(FieldScopeLogic.none())
+ .setIgnoreRepeatedFieldOrderScope(FieldScopeLogic.none())
+ .setIgnoreExtraRepeatedFieldElementsScope(FieldScopeLogic.none())
+ .setDoubleCorrespondenceMap(FieldScopeLogicMap.<Correspondence<Number, Number>>empty())
+ .setFloatCorrespondenceMap(FieldScopeLogicMap.<Correspondence<Number, Number>>empty())
+ .setCompareExpectedFieldsOnly(false)
+ .setHasExpectedMessages(false)
+ .setCompareFieldsScope(FieldScopeLogic.all())
+ .setReportMismatchesOnly(false)
+ .setUnpackingAnyUsing(AnyUtils.defaultTypeRegistry(), AnyUtils.defaultExtensionRegistry())
+ .setUsingCorrespondenceStringFunction(Functions.constant(""))
+ .build();
+
+ static FluentEqualityConfig defaultInstance() {
+ return DEFAULT_INSTANCE;
+ }
+
+ private final LoadingCache<Descriptor, ProtoTruthMessageDifferencer> messageDifferencers =
+ CacheBuilder.newBuilder()
+ .build(
+ new CacheLoader<Descriptor, ProtoTruthMessageDifferencer>() {
+ @Override
+ public ProtoTruthMessageDifferencer load(Descriptor descriptor) {
+ return ProtoTruthMessageDifferencer.create(FluentEqualityConfig.this, descriptor);
+ }
+ });
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // Storage of AbstractProtoFluentEquals configuration data.
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+
+ abstract FieldScopeLogic ignoreFieldAbsenceScope();
+
+ abstract FieldScopeLogic ignoreRepeatedFieldOrderScope();
+
+ abstract FieldScopeLogic ignoreExtraRepeatedFieldElementsScope();
+
+ abstract FieldScopeLogicMap<Correspondence<Number, Number>> doubleCorrespondenceMap();
+
+ abstract FieldScopeLogicMap<Correspondence<Number, Number>> floatCorrespondenceMap();
+
+
+ abstract boolean compareExpectedFieldsOnly();
+
+ // Whether 'withExpectedMessages()' has been invoked. This is a book-keeping boolean to ensure
+ // that 'compareExpectedFieldsOnly()' functions properly; we check that the expected messages are
+ // provided before we do any diffing, as an internal sanity check.
+ abstract boolean hasExpectedMessages();
+
+ abstract FieldScopeLogic compareFieldsScope();
+
+ abstract boolean reportMismatchesOnly();
+
+ abstract TypeRegistry useTypeRegistry();
+
+ abstract ExtensionRegistry useExtensionRegistry();
+
+ // For pretty-printing, does not affect behavior.
+ abstract Function<? super Optional<Descriptor>, String> usingCorrespondenceStringFunction();
+
+ final String usingCorrespondenceString(Optional<Descriptor> descriptor) {
+ return usingCorrespondenceStringFunction().apply(descriptor);
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // Mutators of FluentEqualityConfig configuration data.
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+
+ final FluentEqualityConfig ignoringFieldAbsence() {
+ return toBuilder()
+ .setIgnoreFieldAbsenceScope(FieldScopeLogic.all())
+ .addUsingCorrespondenceString(".ignoringFieldAbsence()")
+ .build();
+ }
+
+ final FluentEqualityConfig ignoringFieldAbsenceOfFields(Iterable<Integer> fieldNumbers) {
+ return toBuilder()
+ .setIgnoreFieldAbsenceScope(
+ ignoreFieldAbsenceScope().allowingFieldsNonRecursive(fieldNumbers))
+ .addUsingCorrespondenceFieldNumbersString(".ignoringFieldAbsenceOf(%s)", fieldNumbers)
+ .build();
+ }
+
+ final FluentEqualityConfig ignoringFieldAbsenceOfFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return toBuilder()
+ .setIgnoreFieldAbsenceScope(
+ ignoreFieldAbsenceScope().allowingFieldDescriptorsNonRecursive(fieldDescriptors))
+ .addUsingCorrespondenceFieldDescriptorsString(
+ ".ignoringFieldAbsenceOf(%s)", fieldDescriptors)
+ .build();
+ }
+
+ final FluentEqualityConfig ignoringRepeatedFieldOrder() {
+ return toBuilder()
+ .setIgnoreRepeatedFieldOrderScope(FieldScopeLogic.all())
+ .addUsingCorrespondenceString(".ignoringRepeatedFieldOrder()")
+ .build();
+ }
+
+ final FluentEqualityConfig ignoringRepeatedFieldOrderOfFields(Iterable<Integer> fieldNumbers) {
+ return toBuilder()
+ .setIgnoreRepeatedFieldOrderScope(
+ ignoreRepeatedFieldOrderScope().allowingFieldsNonRecursive(fieldNumbers))
+ .addUsingCorrespondenceFieldNumbersString(".ignoringRepeatedFieldOrderOf(%s)", fieldNumbers)
+ .build();
+ }
+
+ final FluentEqualityConfig ignoringRepeatedFieldOrderOfFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return toBuilder()
+ .setIgnoreRepeatedFieldOrderScope(
+ ignoreRepeatedFieldOrderScope().allowingFieldDescriptorsNonRecursive(fieldDescriptors))
+ .addUsingCorrespondenceFieldDescriptorsString(
+ ".ignoringRepeatedFieldOrderOf(%s)", fieldDescriptors)
+ .build();
+ }
+
+ final FluentEqualityConfig ignoringExtraRepeatedFieldElements() {
+ return toBuilder()
+ .setIgnoreExtraRepeatedFieldElementsScope(FieldScopeLogic.all())
+ .addUsingCorrespondenceString(".ignoringExtraRepeatedFieldElements()")
+ .build();
+ }
+
+ final FluentEqualityConfig ignoringExtraRepeatedFieldElementsOfFields(
+ Iterable<Integer> fieldNumbers) {
+ return toBuilder()
+ .setIgnoreExtraRepeatedFieldElementsScope(
+ ignoreExtraRepeatedFieldElementsScope().allowingFieldsNonRecursive(fieldNumbers))
+ .addUsingCorrespondenceFieldNumbersString(
+ ".ignoringExtraRepeatedFieldElements(%s)", fieldNumbers)
+ .build();
+ }
+
+ final FluentEqualityConfig ignoringExtraRepeatedFieldElementsOfFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return toBuilder()
+ .setIgnoreExtraRepeatedFieldElementsScope(
+ ignoreExtraRepeatedFieldElementsScope()
+ .allowingFieldDescriptorsNonRecursive(fieldDescriptors))
+ .addUsingCorrespondenceFieldDescriptorsString(
+ ".ignoringExtraRepeatedFieldElements(%s)", fieldDescriptors)
+ .build();
+ }
+
+ final FluentEqualityConfig usingDoubleTolerance(double tolerance) {
+ return toBuilder()
+ .setDoubleCorrespondenceMap(
+ FieldScopeLogicMap.defaultValue(Correspondence.tolerance(tolerance)))
+ .addUsingCorrespondenceString(".usingDoubleTolerance(" + tolerance + ")")
+ .build();
+ }
+
+ final FluentEqualityConfig usingDoubleToleranceForFields(
+ double tolerance, Iterable<Integer> fieldNumbers) {
+ return toBuilder()
+ .setDoubleCorrespondenceMap(
+ doubleCorrespondenceMap()
+ .with(
+ FieldScopeLogic.none().allowingFieldsNonRecursive(fieldNumbers),
+ Correspondence.tolerance(tolerance)))
+ .addUsingCorrespondenceFieldNumbersString(
+ ".usingDoubleTolerance(" + tolerance + ", %s)", fieldNumbers)
+ .build();
+ }
+
+ final FluentEqualityConfig usingDoubleToleranceForFieldDescriptors(
+ double tolerance, Iterable<FieldDescriptor> fieldDescriptors) {
+ return toBuilder()
+ .setDoubleCorrespondenceMap(
+ doubleCorrespondenceMap()
+ .with(
+ FieldScopeLogic.none().allowingFieldDescriptorsNonRecursive(fieldDescriptors),
+ Correspondence.tolerance(tolerance)))
+ .addUsingCorrespondenceFieldDescriptorsString(
+ ".usingDoubleTolerance(" + tolerance + ", %s)", fieldDescriptors)
+ .build();
+ }
+
+ final FluentEqualityConfig usingFloatTolerance(float tolerance) {
+ return toBuilder()
+ .setFloatCorrespondenceMap(
+ FieldScopeLogicMap.defaultValue(Correspondence.tolerance(tolerance)))
+ .addUsingCorrespondenceString(".usingFloatTolerance(" + tolerance + ")")
+ .build();
+ }
+
+ final FluentEqualityConfig usingFloatToleranceForFields(
+ float tolerance, Iterable<Integer> fieldNumbers) {
+ return toBuilder()
+ .setFloatCorrespondenceMap(
+ floatCorrespondenceMap()
+ .with(
+ FieldScopeLogic.none().allowingFieldsNonRecursive(fieldNumbers),
+ Correspondence.tolerance(tolerance)))
+ .addUsingCorrespondenceFieldNumbersString(
+ ".usingFloatTolerance(" + tolerance + ", %s)", fieldNumbers)
+ .build();
+ }
+
+ final FluentEqualityConfig usingFloatToleranceForFieldDescriptors(
+ float tolerance, Iterable<FieldDescriptor> fieldDescriptors) {
+ return toBuilder()
+ .setFloatCorrespondenceMap(
+ floatCorrespondenceMap()
+ .with(
+ FieldScopeLogic.none().allowingFieldDescriptorsNonRecursive(fieldDescriptors),
+ Correspondence.tolerance(tolerance)))
+ .addUsingCorrespondenceFieldDescriptorsString(
+ ".usingFloatTolerance(" + tolerance + ", %s)", fieldDescriptors)
+ .build();
+ }
+
+ final FluentEqualityConfig comparingExpectedFieldsOnly() {
+ return toBuilder()
+ .setCompareExpectedFieldsOnly(true)
+ .addUsingCorrespondenceString(".comparingExpectedFieldsOnly()")
+ .build();
+ }
+
+ final FluentEqualityConfig withExpectedMessages(Iterable<? extends Message> messages) {
+ Builder builder = toBuilder().setHasExpectedMessages(true);
+ if (compareExpectedFieldsOnly()) {
+ builder.setCompareFieldsScope(
+ FieldScopeLogic.and(compareFieldsScope(), FieldScopes.fromSetFields(messages).logic()));
+ }
+ return builder.build();
+ }
+
+ final FluentEqualityConfig withPartialScope(FieldScope partialScope) {
+ return toBuilder()
+ .setCompareFieldsScope(FieldScopeLogic.and(compareFieldsScope(), partialScope.logic()))
+ .addUsingCorrespondenceFieldScopeString(".withPartialScope(%s)", partialScope)
+ .build();
+ }
+
+ final FluentEqualityConfig ignoringFields(Iterable<Integer> fieldNumbers) {
+ return toBuilder()
+ .setCompareFieldsScope(compareFieldsScope().ignoringFields(fieldNumbers))
+ .addUsingCorrespondenceFieldNumbersString(".ignoringFields(%s)", fieldNumbers)
+ .build();
+ }
+
+ final FluentEqualityConfig ignoringFieldDescriptors(Iterable<FieldDescriptor> fieldDescriptors) {
+ return toBuilder()
+ .setCompareFieldsScope(compareFieldsScope().ignoringFieldDescriptors(fieldDescriptors))
+ .addUsingCorrespondenceFieldDescriptorsString(
+ ".ignoringFieldDescriptors(%s)", fieldDescriptors)
+ .build();
+ }
+
+ final FluentEqualityConfig ignoringFieldScope(FieldScope fieldScope) {
+ return toBuilder()
+ .setCompareFieldsScope(
+ FieldScopeLogic.and(compareFieldsScope(), FieldScopeLogic.not(fieldScope.logic())))
+ .addUsingCorrespondenceFieldScopeString(".ignoringFieldScope(%s)", fieldScope)
+ .build();
+ }
+
+ final FluentEqualityConfig reportingMismatchesOnly() {
+ return toBuilder()
+ .setReportMismatchesOnly(true)
+ .addUsingCorrespondenceString(".reportingMismatchesOnly()")
+ .build();
+ }
+
+ final FluentEqualityConfig unpackingAnyUsing(
+ TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry) {
+ return toBuilder()
+ .setUnpackingAnyUsing(typeRegistry, extensionRegistry)
+ .addUsingCorrespondenceString(
+ ".unpackingAnyUsing(" + typeRegistry + ", " + extensionRegistry + ")")
+ .build();
+ }
+
+ @Override
+ public final FluentEqualityConfig subScope(Descriptor rootDescriptor, SubScopeId subScopeId) {
+ return toBuilder()
+ .setIgnoreFieldAbsenceScope(ignoreFieldAbsenceScope().subScope(rootDescriptor, subScopeId))
+ .setIgnoreRepeatedFieldOrderScope(
+ ignoreRepeatedFieldOrderScope().subScope(rootDescriptor, subScopeId))
+ .setIgnoreExtraRepeatedFieldElementsScope(
+ ignoreExtraRepeatedFieldElementsScope().subScope(rootDescriptor, subScopeId))
+ .setDoubleCorrespondenceMap(doubleCorrespondenceMap().subScope(rootDescriptor, subScopeId))
+ .setFloatCorrespondenceMap(floatCorrespondenceMap().subScope(rootDescriptor, subScopeId))
+ .setCompareFieldsScope(compareFieldsScope().subScope(rootDescriptor, subScopeId))
+ .build();
+ }
+
+ @Override
+ public final void validate(
+ Descriptor rootDescriptor, FieldDescriptorValidator fieldDescriptorValidator) {
+ // FluentEqualityConfig should never be validated other than as a root entity.
+ Verify.verify(fieldDescriptorValidator == FieldDescriptorValidator.ALLOW_ALL);
+
+ ignoreFieldAbsenceScope()
+ .validate(rootDescriptor, FieldDescriptorValidator.IS_FIELD_WITH_ABSENCE);
+ ignoreRepeatedFieldOrderScope()
+ .validate(rootDescriptor, FieldDescriptorValidator.IS_FIELD_WITH_ORDER);
+ ignoreExtraRepeatedFieldElementsScope()
+ .validate(rootDescriptor, FieldDescriptorValidator.IS_FIELD_WITH_EXTRA_ELEMENTS);
+ doubleCorrespondenceMap().validate(rootDescriptor, FieldDescriptorValidator.IS_DOUBLE_FIELD);
+ floatCorrespondenceMap().validate(rootDescriptor, FieldDescriptorValidator.IS_FLOAT_FIELD);
+ compareFieldsScope().validate(rootDescriptor, FieldDescriptorValidator.ALLOW_ALL);
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // Converters into comparison utilities.
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+
+ final ProtoTruthMessageDifferencer toMessageDifferencer(Descriptor descriptor) {
+ checkState(hasExpectedMessages(), "withExpectedMessages() not called");
+ return messageDifferencers.getUnchecked(descriptor);
+ }
+
+ final <M extends Message> Correspondence<M, M> toCorrespondence(
+ final Optional<Descriptor> optDescriptor) {
+ checkState(hasExpectedMessages(), "withExpectedMessages() not called");
+ return Correspondence.from(
+ // If we were allowed lambdas, this would be:
+ // (M a, M e) ->
+ // ProtoTruth.assertThat(a).usingConfig(FluentEqualityConfig.this).testIsEqualTo(e),
+ new Correspondence.BinaryPredicate<M, M>() {
+ @Override
+ public boolean apply(@Nullable M actual, @Nullable M expected) {
+ return ProtoTruth.assertThat(actual)
+ .usingConfig(FluentEqualityConfig.this)
+ .testIsEqualTo(expected);
+ }
+ },
+ "is equivalent according to assertThat(proto)"
+ + usingCorrespondenceString(optDescriptor)
+ + ".isEqualTo(target) to")
+ .formattingDiffsUsing(
+ // If we were allowed method references, this would be this::formatDiff.
+ new Correspondence.DiffFormatter<M, M>() {
+ @Override
+ public String formatDiff(@Nullable M actual, @Nullable M expected) {
+ return FluentEqualityConfig.this.formatDiff(actual, expected);
+ }
+ });
+ }
+
+ private <M extends Message> String formatDiff(@Nullable M actual, @Nullable M expected) {
+ if (actual == null || expected == null) {
+ return "";
+ }
+
+ return toMessageDifferencer(actual.getDescriptorForType())
+ .diffMessages(actual, expected)
+ .printToString(reportMismatchesOnly());
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // Builder methods.
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+
+ abstract Builder toBuilder();
+
+ @CanIgnoreReturnValue
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder setIgnoreFieldAbsenceScope(FieldScopeLogic fieldScopeLogic);
+
+ abstract Builder setIgnoreRepeatedFieldOrderScope(FieldScopeLogic fieldScopeLogic);
+
+ abstract Builder setIgnoreExtraRepeatedFieldElementsScope(FieldScopeLogic fieldScopeLogic);
+
+ abstract Builder setDoubleCorrespondenceMap(
+ FieldScopeLogicMap<Correspondence<Number, Number>> doubleCorrespondenceMap);
+
+ abstract Builder setFloatCorrespondenceMap(
+ FieldScopeLogicMap<Correspondence<Number, Number>> floatCorrespondenceMap);
+
+
+ abstract Builder setCompareExpectedFieldsOnly(boolean compare);
+
+ abstract Builder setHasExpectedMessages(boolean hasExpectedMessages);
+
+ abstract Builder setCompareFieldsScope(FieldScopeLogic fieldScopeLogic);
+
+ abstract Builder setReportMismatchesOnly(boolean reportMismatchesOnly);
+
+ final Builder setUnpackingAnyUsing(
+ TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry) {
+ setUseTypeRegistry(typeRegistry);
+ setUseExtensionRegistry(extensionRegistry);
+ return this;
+ }
+
+ abstract Builder setUseTypeRegistry(TypeRegistry typeRegistry);
+
+ abstract Builder setUseExtensionRegistry(ExtensionRegistry extensionRegistry);
+
+ @CheckReturnValue
+ abstract Function<? super Optional<Descriptor>, String> usingCorrespondenceStringFunction();
+
+ abstract Builder setUsingCorrespondenceStringFunction(
+ Function<? super Optional<Descriptor>, String> usingCorrespondenceStringFunction);
+
+ abstract FluentEqualityConfig build();
+
+ // Lazy formatting methods.
+ // These allow us to print raw integer field numbers with meaningful names.
+
+ final Builder addUsingCorrespondenceString(String string) {
+ return setUsingCorrespondenceStringFunction(
+ FieldScopeUtil.concat(usingCorrespondenceStringFunction(), Functions.constant(string)));
+ }
+
+ final Builder addUsingCorrespondenceFieldNumbersString(
+ String fmt, Iterable<Integer> fieldNumbers) {
+ return setUsingCorrespondenceStringFunction(
+ FieldScopeUtil.concat(
+ usingCorrespondenceStringFunction(),
+ FieldScopeUtil.fieldNumbersFunction(fmt, fieldNumbers)));
+ }
+
+ final Builder addUsingCorrespondenceFieldDescriptorsString(
+ String fmt, Iterable<FieldDescriptor> fieldDescriptors) {
+ return setUsingCorrespondenceStringFunction(
+ FieldScopeUtil.concat(
+ usingCorrespondenceStringFunction(),
+ Functions.constant(String.format(fmt, join(fieldDescriptors)))));
+ }
+
+ final Builder addUsingCorrespondenceFieldScopeString(String fmt, FieldScope fieldScope) {
+ return setUsingCorrespondenceStringFunction(
+ FieldScopeUtil.concat(
+ usingCorrespondenceStringFunction(),
+ FieldScopeUtil.fieldScopeFunction(fmt, fieldScope)));
+ }
+ }
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/IterableOfProtosFluentAssertion.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/IterableOfProtosFluentAssertion.java
new file mode 100644
index 00000000..e90a32c4
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/IterableOfProtosFluentAssertion.java
@@ -0,0 +1,503 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.ExtensionRegistry;
+import com.google.protobuf.Message;
+import com.google.protobuf.TypeRegistry;
+
+/**
+ * Fluent API to perform detailed, customizable comparison of iterables of protocol buffers. The
+ * same comparison rules are applied to all pairs of protocol buffers which get compared.
+ *
+ * <p>Methods may be chained in any order, but the chain should terminate with a method from {@link
+ * IterableOfProtosUsingCorrespondence}.
+ *
+ * <p>The state of an {@code IterableOfProtosFluentAssertion} object after each method is called is
+ * left undefined. Users should not retain references to {@code IterableOfProtosFluentAssertion}
+ * instances.
+ */
+public interface IterableOfProtosFluentAssertion<M extends Message>
+ extends IterableOfProtosUsingCorrespondence<M> {
+
+ /**
+ * Specifies that the 'has' bit of individual fields should be ignored when comparing for
+ * equality.
+ *
+ * <p>For version 2 Protocol Buffers, this setting determines whether two protos with the same
+ * value for a field compare equal if one explicitly sets the value, and the other merely
+ * implicitly uses the schema-defined default. This setting also determines whether unknown fields
+ * should be considered in the comparison. By {@code ignoringFieldAbsence()}, unknown fields are
+ * ignored, and value-equal fields as specified above are considered equal.
+ *
+ * <p>For version 3 Protocol Buffers, this setting does not affect primitive fields, because their
+ * default value is indistinguishable from unset.
+ */
+ IterableOfProtosFluentAssertion<M> ignoringFieldAbsence();
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified top-level field numbers should be
+ * ignored when comparing for equality. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if they are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsence()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsence() for details
+ */
+ IterableOfProtosFluentAssertion<M> ignoringFieldAbsenceOfFields(
+ int firstFieldNumber, int... rest);
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified top-level field numbers should be
+ * ignored when comparing for equality. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if they are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsence()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsence() for details
+ */
+ IterableOfProtosFluentAssertion<M> ignoringFieldAbsenceOfFields(Iterable<Integer> fieldNumbers);
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified field descriptors should be ignored
+ * when comparing for equality. Sub-fields must be specified explicitly if they are to be ignored
+ * as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsence()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsence() for details
+ */
+ IterableOfProtosFluentAssertion<M> ignoringFieldAbsenceOfFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified field descriptors should be ignored
+ * when comparing for equality. Sub-fields must be specified explicitly if they are to be ignored
+ * as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsence()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsence() for details
+ */
+ IterableOfProtosFluentAssertion<M> ignoringFieldAbsenceOfFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Specifies that the ordering of repeated fields, at all levels, should be ignored when comparing
+ * for equality.
+ *
+ * <p>This setting applies to all repeated fields recursively, but it does not ignore structure.
+ * For example, with {@link #ignoringRepeatedFieldOrder()}, a repeated {@code int32} field {@code
+ * bar}, set inside a repeated message field {@code foo}, the following protos will all compare
+ * equal:
+ *
+ * <pre>{@code
+ * message1: {
+ * foo: {
+ * bar: 1
+ * bar: 2
+ * }
+ * foo: {
+ * bar: 3
+ * bar: 4
+ * }
+ * }
+ *
+ * message2: {
+ * foo: {
+ * bar: 2
+ * bar: 1
+ * }
+ * foo: {
+ * bar: 4
+ * bar: 3
+ * }
+ * }
+ *
+ * message3: {
+ * foo: {
+ * bar: 4
+ * bar: 3
+ * }
+ * foo: {
+ * bar: 2
+ * bar: 1
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>However, the following message will compare equal to none of these:
+ *
+ * <pre>{@code
+ * message4: {
+ * foo: {
+ * bar: 1
+ * bar: 3
+ * }
+ * foo: {
+ * bar: 2
+ * bar: 4
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>This setting does not apply to map fields, for which field order is always ignored. The
+ * serialization order of map fields is undefined, and it may change from runtime to runtime.
+ */
+ IterableOfProtosFluentAssertion<M> ignoringRepeatedFieldOrder();
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified top-level field
+ * numbers should be ignored when comparing for equality. Sub-fields must be specified explicitly
+ * (via {@link FieldDescriptor}) if their orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrder()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrder() for details.
+ */
+ IterableOfProtosFluentAssertion<M> ignoringRepeatedFieldOrderOfFields(
+ int firstFieldNumber, int... rest);
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified top-level field
+ * numbers should be ignored when comparing for equality. Sub-fields must be specified explicitly
+ * (via {@link FieldDescriptor}) if their orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrder()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrder() for details.
+ */
+ IterableOfProtosFluentAssertion<M> ignoringRepeatedFieldOrderOfFields(
+ Iterable<Integer> fieldNumbers);
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified field descriptors
+ * should be ignored when comparing for equality. Sub-fields must be specified explicitly if their
+ * orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrder()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrder() for details.
+ */
+ IterableOfProtosFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified field descriptors
+ * should be ignored when comparing for equality. Sub-fields must be specified explicitly if their
+ * orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrder()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrder() for details.
+ */
+ IterableOfProtosFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Specifies that, for all repeated and map fields, any elements in the 'actual' proto which are
+ * not found in the 'expected' proto are ignored, with the exception of fields in the expected
+ * proto which are empty. To ignore empty repeated fields as well, use {@link
+ * #comparingExpectedFieldsOnly}.
+ *
+ * <p>This rule is applied independently from {@link #ignoringRepeatedFieldOrder}. If ignoring
+ * repeated field order AND extra repeated field elements, all that is tested is that the expected
+ * elements comprise a subset of the actual elements. If not ignoring repeated field order, but
+ * still ignoring extra repeated field elements, the actual elements must contain a subsequence
+ * that matches the expected elements for the test to pass. (The subsequence rule does not apply
+ * to Map fields, which are always compared by key.)
+ */
+ IterableOfProtosFluentAssertion<M> ignoringExtraRepeatedFieldElements();
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified top-level field
+ * numbers should be ignored. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if their extra elements are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElements()} instead to ignore these for all fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElements() for details.
+ */
+ IterableOfProtosFluentAssertion<M> ignoringExtraRepeatedFieldElementsOfFields(
+ int firstFieldNumber, int... rest);
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified top-level field
+ * numbers should be ignored. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if their extra elements are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElements()} instead to ignore these for all fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElements() for details.
+ */
+ IterableOfProtosFluentAssertion<M> ignoringExtraRepeatedFieldElementsOfFields(
+ Iterable<Integer> fieldNumbers);
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified field descriptors
+ * should be ignored. Sub-fields must be specified explicitly if their extra elements are to be
+ * ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElements()} instead to ignore these for all fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElements() for details.
+ */
+ IterableOfProtosFluentAssertion<M> ignoringExtraRepeatedFieldElementsOfFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified field descriptors
+ * should be ignored. Sub-fields must be specified explicitly if their extra elements are to be
+ * ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElements()} instead to ignore these for all fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElements() for details.
+ */
+ IterableOfProtosFluentAssertion<M> ignoringExtraRepeatedFieldElementsOfFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Compares double fields as equal if they are both finite and their absolute difference is less
+ * than or equal to {@code tolerance}.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ IterableOfProtosFluentAssertion<M> usingDoubleTolerance(double tolerance);
+
+ /**
+ * Compares double fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ IterableOfProtosFluentAssertion<M> usingDoubleToleranceForFields(
+ double tolerance, int firstFieldNumber, int... rest);
+
+ /**
+ * Compares double fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ IterableOfProtosFluentAssertion<M> usingDoubleToleranceForFields(
+ double tolerance, Iterable<Integer> fieldNumbers);
+
+ /**
+ * Compares double fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ IterableOfProtosFluentAssertion<M> usingDoubleToleranceForFieldDescriptors(
+ double tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Compares double fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ IterableOfProtosFluentAssertion<M> usingDoubleToleranceForFieldDescriptors(
+ double tolerance, Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Compares float fields as equal if they are both finite and their absolute difference is less
+ * than or equal to {@code tolerance}.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ IterableOfProtosFluentAssertion<M> usingFloatTolerance(float tolerance);
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ IterableOfProtosFluentAssertion<M> usingFloatToleranceForFields(
+ float tolerance, int firstFieldNumber, int... rest);
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ IterableOfProtosFluentAssertion<M> usingFloatToleranceForFields(
+ float tolerance, Iterable<Integer> fieldNumbers);
+
+ /**
+ * Compares float fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ IterableOfProtosFluentAssertion<M> usingFloatToleranceForFieldDescriptors(
+ float tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ IterableOfProtosFluentAssertion<M> usingFloatToleranceForFieldDescriptors(
+ float tolerance, Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Limits the comparison of Protocol buffers to the fields set in the expected proto(s). When
+ * multiple protos are specified, the comparison is limited to the union of set fields in all the
+ * expected protos.
+ *
+ * <p>The "expected proto(s)" are those passed to the method in {@link
+ * IterableOfProtosUsingCorrespondence} at the end of the call-chain.
+ *
+ * <p>Fields not set in the expected proto(s) are ignored. In particular, proto3 fields which have
+ * their default values are ignored, as these are indistinguishable from unset fields. If you want
+ * to assert that a proto3 message has certain fields with default values, you cannot use this
+ * method.
+ */
+ IterableOfProtosFluentAssertion<M> comparingExpectedFieldsOnly();
+
+ /**
+ * Limits the comparison of Protocol buffers to the defined {@link FieldScope}.
+ *
+ * <p>This method is additive and has well-defined ordering semantics. If the invoking {@link
+ * IterableOfProtosFluentAssertion} is already scoped to a {@link FieldScope} {@code X}, and this
+ * method is invoked with {@link FieldScope} {@code Y}, the resultant {@link
+ * IterableOfProtosFluentAssertion} is constrained to the intersection of {@link FieldScope}s
+ * {@code X} and {@code Y}.
+ *
+ * <p>By default, {@link IterableOfProtosFluentAssertion} is constrained to {@link
+ * FieldScopes#all()}, that is, no fields are excluded from comparison.
+ */
+ IterableOfProtosFluentAssertion<M> withPartialScope(FieldScope fieldScope);
+
+ /**
+ * Excludes the top-level message fields with the given tag numbers from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * numbers are ignored, and all sub-messages of type {@code M} will also have these field numbers
+ * ignored.
+ *
+ * <p>If an invalid field number is supplied, the terminal comparison operation will throw a
+ * runtime exception.
+ */
+ IterableOfProtosFluentAssertion<M> ignoringFields(int firstFieldNumber, int... rest);
+
+ /**
+ * Excludes the top-level message fields with the given tag numbers from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * numbers are ignored, and all sub-messages of type {@code M} will also have these field numbers
+ * ignored.
+ *
+ * <p>If an invalid field number is supplied, the terminal comparison operation will throw a
+ * runtime exception.
+ */
+ IterableOfProtosFluentAssertion<M> ignoringFields(Iterable<Integer> fieldNumbers);
+
+ /**
+ * Excludes all message fields matching the given {@link FieldDescriptor}s from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * descriptors are ignored, no matter where they occur in the tree.
+ *
+ * <p>If a field descriptor which does not, or cannot occur in the proto structure is supplied, it
+ * is silently ignored.
+ */
+ IterableOfProtosFluentAssertion<M> ignoringFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Excludes all message fields matching the given {@link FieldDescriptor}s from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * descriptors are ignored, no matter where they occur in the tree.
+ *
+ * <p>If a field descriptor which does not, or cannot occur in the proto structure is supplied, it
+ * is silently ignored.
+ */
+ IterableOfProtosFluentAssertion<M> ignoringFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Excludes all specific field paths under the argument {@link FieldScope} from the comparison.
+ *
+ * <p>This method is additive and has well-defined ordering semantics. If the invoking {@link
+ * IterableOfProtosFluentAssertion} is already scoped to a {@link FieldScope} {@code X}, and this
+ * method is invoked with {@link FieldScope} {@code Y}, the resultant {@link
+ * IterableOfProtosFluentAssertion} is constrained to the subtraction of {@code X - Y}.
+ *
+ * <p>By default, {@link IterableOfProtosFluentAssertion} is constrained to {@link
+ * FieldScopes#all()}, that is, no fields are excluded from comparison.
+ */
+ IterableOfProtosFluentAssertion<M> ignoringFieldScope(FieldScope fieldScope);
+
+ /**
+ * If set, in the event of a comparison failure, the error message printed will list only those
+ * specific fields that did not match between the actual and expected values. Useful for very
+ * large protocol buffers.
+ *
+ * <p>This a purely cosmetic setting, and it has no effect on the behavior of the test.
+ */
+ IterableOfProtosFluentAssertion<M> reportingMismatchesOnly();
+
+ /**
+ * Specifies the {@link TypeRegistry} and {@link ExtensionRegistry} to use for {@link
+ * com.google.protobuf.Any Any} messages.
+ *
+ * <p>To compare the value of an {@code Any} message, ProtoTruth looks in the given type registry
+ * for a descriptor for the message's type URL:
+ *
+ * <ul>
+ * <li>If ProtoTruth finds a descriptor, it unpacks the value and compares it against the
+ * expected value, respecting any configuration methods used for the assertion.
+ * <li>If ProtoTruth does not find a descriptor (or if the value can't be deserialized with the
+ * descriptor), it compares the raw, serialized bytes of the expected and actual values.
+ * </ul>
+ *
+ * <p>When ProtoTruth unpacks a value, it is parsing a serialized proto. That proto may contain
+ * extensions. To look up those extensions, ProtoTruth uses the provided {@link
+ * ExtensionRegistry}.
+ *
+ * @since 1.1
+ */
+ IterableOfProtosFluentAssertion<M> unpackingAnyUsing(
+ TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry);
+
+ /**
+ * @deprecated Do not call {@code equals()} on a {@code IterableOfProtosFluentAssertion}.
+ * @see com.google.common.truth.Subject#equals(Object)
+ */
+ @Override
+ @Deprecated
+ boolean equals(Object o);
+
+ /**
+ * @deprecated {@code IterableOfProtosFluentAssertion} does not support {@code hashCode()}.
+ * @see com.google.common.truth.Subject#hashCode()
+ */
+ @Override
+ @Deprecated
+ int hashCode();
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/IterableOfProtosSubject.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/IterableOfProtosSubject.java
new file mode 100644
index 00000000..0b9252a7
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/IterableOfProtosSubject.java
@@ -0,0 +1,1110 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.Lists.asList;
+import static com.google.common.truth.extensions.proto.FieldScopeUtil.asList;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IterableSubject;
+import com.google.common.truth.Ordered;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.ExtensionRegistry;
+import com.google.protobuf.Message;
+import com.google.protobuf.TypeRegistry;
+import java.util.Arrays;
+import java.util.Comparator;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Truth subject for the iterables of protocol buffers.
+ *
+ * <p>{@code ProtoTruth.assertThat(actual).containsExactly(expected)} performs the same assertion as
+ * {@code Truth.assertThat(actual).containsExactly(expected)}. By default, the assertions are strict
+ * with respect to repeated field order, missing fields, etc. This behavior can be changed with the
+ * configuration methods on this subject, e.g. {@code
+ * ProtoTruth.assertThat(actual).ignoringRepeatedFieldOrder().containsExactlyEntriesIn(expected)}.
+ *
+ * <p>By default, floating-point fields are compared using exact equality, which is <a
+ * href="https://truth.dev/floating_point">probably not what you want</a> if the values are the
+ * results of some arithmetic. To check for approximate equality, use {@link #usingDoubleTolerance},
+ * {@link #usingFloatTolerance}, and {@linkplain #usingDoubleToleranceForFields(double, int, int...)
+ * their per-field equivalents}.
+ *
+ * <p>Equality tests, and other methods, may yield slightly different behavior for versions 2 and 3
+ * of Protocol Buffers. If testing protos of multiple versions, make sure you understand the
+ * behaviors of default and unknown fields so you don't under or over test.
+ *
+ * @param <M> the type of the messages in the {@code Iterable}
+ */
+public class IterableOfProtosSubject<M extends Message> extends IterableSubject {
+
+ /*
+ * Storing a FailureMetadata instance in a Subject subclass is generally a bad practice. For an
+ * explanation of why it works out OK here, see LiteProtoSubject.
+ */
+ private final FailureMetadata metadata;
+ private final Iterable<M> actual;
+ private final FluentEqualityConfig config;
+
+ protected IterableOfProtosSubject(
+ FailureMetadata failureMetadata, @Nullable Iterable<M> messages) {
+ this(failureMetadata, FluentEqualityConfig.defaultInstance(), messages);
+ }
+
+ IterableOfProtosSubject(
+ FailureMetadata failureMetadata,
+ FluentEqualityConfig config,
+ @Nullable Iterable<M> messages) {
+ super(failureMetadata, messages);
+ this.metadata = failureMetadata;
+ this.actual = messages;
+ this.config = config;
+ }
+
+ /**
+ * Specifies a way to pair up unexpected and missing elements in the message when an assertion
+ * fails. For example:
+ *
+ * <pre>{@code
+ * assertThat(actualFoos)
+ * .ignoringRepeatedFieldOrder()
+ * .ignoringFields(Foo.BAR_FIELD_NUMBER)
+ * .displayingDiffsPairedBy(Foo::getId)
+ * .containsExactlyElementsIn(expectedFoos);
+ * }</pre>
+ *
+ * <p>On assertions where it makes sense to do so, the elements are paired as follows: they are
+ * keyed by {@code keyFunction}, and if an unexpected element and a missing element have the same
+ * non-null key then the they are paired up. (Elements with null keys are not paired.) The failure
+ * message will show paired elements together, and a diff will be shown.
+ *
+ * <p>The expected elements given in the assertion should be uniquely keyed by {@code
+ * keyFunction}. If multiple missing elements have the same key then the pairing will be skipped.
+ *
+ * <p>Useful key functions will have the property that key equality is less strict than the
+ * already specified equality rules; i.e. given {@code actual} and {@code expected} values with
+ * keys {@code actualKey} and {@code expectedKey}, if {@code actual} and {@code expected} compare
+ * equal given the rest of the directives such as {@code ignoringRepeatedFieldOrder} and {@code
+ * ignoringFields}, then it is guaranteed that {@code actualKey} is equal to {@code expectedKey},
+ * but there are cases where {@code actualKey} is equal to {@code expectedKey} but the direct
+ * comparison fails.
+ *
+ * <p>Note that calling this method makes no difference to whether a test passes or fails, it just
+ * improves the message if it fails.
+ */
+ public IterableOfProtosUsingCorrespondence<M> displayingDiffsPairedBy(
+ Function<? super M, ?> keyFunction) {
+ return usingCorrespondence().displayingDiffsPairedBy(keyFunction);
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // ProtoFluentAssertion Configuration
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+
+ IterableOfProtosFluentAssertion<M> usingConfig(FluentEqualityConfig newConfig) {
+ return new IterableOfProtosFluentAssertionImpl<>(
+ new IterableOfProtosSubject<>(metadata, newConfig, actual));
+ }
+
+ /**
+ * Specifies that the 'has' bit of individual fields should be ignored when comparing for
+ * equality.
+ *
+ * <p>For version 2 Protocol Buffers, this setting determines whether two protos with the same
+ * value for a field compare equal if one explicitly sets the value, and the other merely
+ * implicitly uses the schema-defined default. This setting also determines whether unknown fields
+ * should be considered in the comparison. By {@code ignoringFieldAbsence()}, unknown fields are
+ * ignored, and value-equal fields as specified above are considered equal.
+ *
+ * <p>For version 3 Protocol Buffers, this setting does not affect primitive fields, because their
+ * default value is indistinguishable from unset.
+ */
+ public IterableOfProtosFluentAssertion<M> ignoringFieldAbsence() {
+ return usingConfig(config.ignoringFieldAbsence());
+ }
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified top-level field numbers should be
+ * ignored when comparing for equality. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if they are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsence()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsence() for details
+ */
+ public IterableOfProtosFluentAssertion<M> ignoringFieldAbsenceOfFields(
+ int firstFieldNumber, int... rest) {
+ return usingConfig(config.ignoringFieldAbsenceOfFields(asList(firstFieldNumber, rest)));
+ }
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified top-level field numbers should be
+ * ignored when comparing for equality. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if they are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsence()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsence() for details
+ */
+ public IterableOfProtosFluentAssertion<M> ignoringFieldAbsenceOfFields(
+ Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.ignoringFieldAbsenceOfFields(fieldNumbers));
+ }
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified field descriptors should be ignored
+ * when comparing for equality. Sub-fields must be specified explicitly if they are to be ignored
+ * as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsence()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsence() for details
+ */
+ public IterableOfProtosFluentAssertion<M> ignoringFieldAbsenceOfFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return usingConfig(
+ config.ignoringFieldAbsenceOfFieldDescriptors(asList(firstFieldDescriptor, rest)));
+ }
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified field descriptors should be ignored
+ * when comparing for equality. Sub-fields must be specified explicitly if they are to be ignored
+ * as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsence()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsence() for details
+ */
+ public IterableOfProtosFluentAssertion<M> ignoringFieldAbsenceOfFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(config.ignoringFieldAbsenceOfFieldDescriptors(fieldDescriptors));
+ }
+
+ /**
+ * Specifies that the ordering of repeated fields, at all levels, should be ignored when comparing
+ * for equality.
+ *
+ * <p>This setting applies to all repeated fields recursively, but it does not ignore structure.
+ * For example, with {@link #ignoringRepeatedFieldOrder()}, a repeated {@code int32} field {@code
+ * bar}, set inside a repeated message field {@code foo}, the following protos will all compare
+ * equal:
+ *
+ * <pre>{@code
+ * message1: {
+ * foo: {
+ * bar: 1
+ * bar: 2
+ * }
+ * foo: {
+ * bar: 3
+ * bar: 4
+ * }
+ * }
+ *
+ * message2: {
+ * foo: {
+ * bar: 2
+ * bar: 1
+ * }
+ * foo: {
+ * bar: 4
+ * bar: 3
+ * }
+ * }
+ *
+ * message3: {
+ * foo: {
+ * bar: 4
+ * bar: 3
+ * }
+ * foo: {
+ * bar: 2
+ * bar: 1
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>However, the following message will compare equal to none of these:
+ *
+ * <pre>{@code
+ * message4: {
+ * foo: {
+ * bar: 1
+ * bar: 3
+ * }
+ * foo: {
+ * bar: 2
+ * bar: 4
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>This setting does not apply to map fields, for which field order is always ignored. The
+ * serialization order of map fields is undefined, and it may change from runtime to runtime.
+ */
+ public IterableOfProtosFluentAssertion<M> ignoringRepeatedFieldOrder() {
+ return usingConfig(config.ignoringRepeatedFieldOrder());
+ }
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified top-level field
+ * numbers should be ignored when comparing for equality. Sub-fields must be specified explicitly
+ * (via {@link FieldDescriptor}) if their orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrder()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrder() for details.
+ */
+ public IterableOfProtosFluentAssertion<M> ignoringRepeatedFieldOrderOfFields(
+ int firstFieldNumber, int... rest) {
+ return usingConfig(config.ignoringRepeatedFieldOrderOfFields(asList(firstFieldNumber, rest)));
+ }
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified top-level field
+ * numbers should be ignored when comparing for equality. Sub-fields must be specified explicitly
+ * (via {@link FieldDescriptor}) if their orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrder()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrder() for details.
+ */
+ public IterableOfProtosFluentAssertion<M> ignoringRepeatedFieldOrderOfFields(
+ Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.ignoringRepeatedFieldOrderOfFields(fieldNumbers));
+ }
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified field descriptors
+ * should be ignored when comparing for equality. Sub-fields must be specified explicitly if their
+ * orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrder()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrder() for details.
+ */
+ public IterableOfProtosFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return usingConfig(
+ config.ignoringRepeatedFieldOrderOfFieldDescriptors(asList(firstFieldDescriptor, rest)));
+ }
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified field descriptors
+ * should be ignored when comparing for equality. Sub-fields must be specified explicitly if their
+ * orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrder()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrder() for details.
+ */
+ public IterableOfProtosFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(config.ignoringRepeatedFieldOrderOfFieldDescriptors(fieldDescriptors));
+ }
+
+ /**
+ * Specifies that, for all repeated and map fields, any elements in the 'actual' proto which are
+ * not found in the 'expected' proto are ignored, with the exception of fields in the expected
+ * proto which are empty. To ignore empty repeated fields as well, use {@link
+ * #comparingExpectedFieldsOnly}.
+ *
+ * <p>This rule is applied independently from {@link #ignoringRepeatedFieldOrder}. If ignoring
+ * repeated field order AND extra repeated field elements, all that is tested is that the expected
+ * elements comprise a subset of the actual elements. If not ignoring repeated field order, but
+ * still ignoring extra repeated field elements, the actual elements must contain a subsequence
+ * that matches the expected elements for the test to pass. (The subsequence rule does not apply
+ * to Map fields, which are always compared by key.)
+ */
+ public IterableOfProtosFluentAssertion<M> ignoringExtraRepeatedFieldElements() {
+ return usingConfig(config.ignoringExtraRepeatedFieldElements());
+ }
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified top-level field
+ * numbers should be ignored. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if their extra elements are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElements()} instead to ignore these for all fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElements() for details.
+ */
+ public IterableOfProtosFluentAssertion<M> ignoringExtraRepeatedFieldElementsOfFields(
+ int firstFieldNumber, int... rest) {
+ return usingConfig(
+ config.ignoringExtraRepeatedFieldElementsOfFields(asList(firstFieldNumber, rest)));
+ }
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified top-level field
+ * numbers should be ignored. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if their extra elements are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElements()} instead to ignore these for all fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElements() for details.
+ */
+ public IterableOfProtosFluentAssertion<M> ignoringExtraRepeatedFieldElementsOfFields(
+ Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.ignoringExtraRepeatedFieldElementsOfFields(fieldNumbers));
+ }
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified field descriptors
+ * should be ignored. Sub-fields must be specified explicitly if their extra elements are to be
+ * ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElements()} instead to ignore these for all fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElements() for details.
+ */
+ public IterableOfProtosFluentAssertion<M> ignoringExtraRepeatedFieldElementsOfFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return usingConfig(
+ config.ignoringExtraRepeatedFieldElementsOfFieldDescriptors(
+ asList(firstFieldDescriptor, rest)));
+ }
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified field descriptors
+ * should be ignored. Sub-fields must be specified explicitly if their extra elements are to be
+ * ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElements()} instead to ignore these for all fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElements() for details.
+ */
+ public IterableOfProtosFluentAssertion<M> ignoringExtraRepeatedFieldElementsOfFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(
+ config.ignoringExtraRepeatedFieldElementsOfFieldDescriptors(fieldDescriptors));
+ }
+
+ /**
+ * Compares double fields as equal if they are both finite and their absolute difference is less
+ * than or equal to {@code tolerance}.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public IterableOfProtosFluentAssertion<M> usingDoubleTolerance(double tolerance) {
+ return usingConfig(config.usingDoubleTolerance(tolerance));
+ }
+
+ /**
+ * Compares double fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public IterableOfProtosFluentAssertion<M> usingDoubleToleranceForFields(
+ double tolerance, int firstFieldNumber, int... rest) {
+ return usingConfig(
+ config.usingDoubleToleranceForFields(tolerance, asList(firstFieldNumber, rest)));
+ }
+
+ /**
+ * Compares double fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public IterableOfProtosFluentAssertion<M> usingDoubleToleranceForFields(
+ double tolerance, Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.usingDoubleToleranceForFields(tolerance, fieldNumbers));
+ }
+
+ /**
+ * Compares double fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public IterableOfProtosFluentAssertion<M> usingDoubleToleranceForFieldDescriptors(
+ double tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return usingConfig(
+ config.usingDoubleToleranceForFieldDescriptors(
+ tolerance, asList(firstFieldDescriptor, rest)));
+ }
+
+ /**
+ * Compares double fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public IterableOfProtosFluentAssertion<M> usingDoubleToleranceForFieldDescriptors(
+ double tolerance, Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(config.usingDoubleToleranceForFieldDescriptors(tolerance, fieldDescriptors));
+ }
+
+ /**
+ * Compares float fields as equal if they are both finite and their absolute difference is less
+ * than or equal to {@code tolerance}.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public IterableOfProtosFluentAssertion<M> usingFloatTolerance(float tolerance) {
+ return usingConfig(config.usingFloatTolerance(tolerance));
+ }
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public IterableOfProtosFluentAssertion<M> usingFloatToleranceForFields(
+ float tolerance, int firstFieldNumber, int... rest) {
+ return usingConfig(
+ config.usingFloatToleranceForFields(tolerance, asList(firstFieldNumber, rest)));
+ }
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public IterableOfProtosFluentAssertion<M> usingFloatToleranceForFields(
+ float tolerance, Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.usingFloatToleranceForFields(tolerance, fieldNumbers));
+ }
+
+ /**
+ * Compares float fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public IterableOfProtosFluentAssertion<M> usingFloatToleranceForFieldDescriptors(
+ float tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return usingConfig(
+ config.usingFloatToleranceForFieldDescriptors(
+ tolerance, asList(firstFieldDescriptor, rest)));
+ }
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public IterableOfProtosFluentAssertion<M> usingFloatToleranceForFieldDescriptors(
+ float tolerance, Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(config.usingFloatToleranceForFieldDescriptors(tolerance, fieldDescriptors));
+ }
+
+ /**
+ * Limits the comparison of Protocol buffers to the fields set in the expected proto(s). When
+ * multiple protos are specified, the comparison is limited to the union of set fields in all the
+ * expected protos.
+ *
+ * <p>The "expected proto(s)" are those passed to the method in {@link
+ * IterableOfProtosUsingCorrespondence} at the end of the call-chain.
+ *
+ * <p>Fields not set in the expected proto(s) are ignored. In particular, proto3 fields which have
+ * their default values are ignored, as these are indistinguishable from unset fields. If you want
+ * to assert that a proto3 message has certain fields with default values, you cannot use this
+ * method.
+ */
+ public IterableOfProtosFluentAssertion<M> comparingExpectedFieldsOnly() {
+ return usingConfig(config.comparingExpectedFieldsOnly());
+ }
+
+ /**
+ * Limits the comparison of Protocol buffers to the defined {@link FieldScope}.
+ *
+ * <p>This method is additive and has well-defined ordering semantics. If the invoking {@link
+ * ProtoFluentAssertion} is already scoped to a {@link FieldScope} {@code X}, and this method is
+ * invoked with {@link FieldScope} {@code Y}, the resultant {@link ProtoFluentAssertion} is
+ * constrained to the intersection of {@link FieldScope}s {@code X} and {@code Y}.
+ *
+ * <p>By default, {@link ProtoFluentAssertion} is constrained to {@link FieldScopes#all()}, that
+ * is, no fields are excluded from comparison.
+ */
+ public IterableOfProtosFluentAssertion<M> withPartialScope(FieldScope fieldScope) {
+ return usingConfig(config.withPartialScope(checkNotNull(fieldScope, "fieldScope")));
+ }
+
+ /**
+ * Excludes the top-level message fields with the given tag numbers from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * numbers are ignored, and all sub-messages of type {@code M} will also have these field numbers
+ * ignored.
+ *
+ * <p>If an invalid field number is supplied, the terminal comparison operation will throw a
+ * runtime exception.
+ */
+ public IterableOfProtosFluentAssertion<M> ignoringFields(int firstFieldNumber, int... rest) {
+ return ignoringFields(asList(firstFieldNumber, rest));
+ }
+
+ /**
+ * Excludes the top-level message fields with the given tag numbers from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * numbers are ignored, and all sub-messages of type {@code M} will also have these field numbers
+ * ignored.
+ *
+ * <p>If an invalid field number is supplied, the terminal comparison operation will throw a
+ * runtime exception.
+ */
+ public IterableOfProtosFluentAssertion<M> ignoringFields(Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.ignoringFields(fieldNumbers));
+ }
+
+ /**
+ * Excludes all message fields matching the given {@link FieldDescriptor}s from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * descriptors are ignored, no matter where they occur in the tree.
+ *
+ * <p>If a field descriptor which does not, or cannot occur in the proto structure is supplied, it
+ * is silently ignored.
+ */
+ public IterableOfProtosFluentAssertion<M> ignoringFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return ignoringFieldDescriptors(asList(firstFieldDescriptor, rest));
+ }
+
+ /**
+ * Excludes all message fields matching the given {@link FieldDescriptor}s from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * descriptors are ignored, no matter where they occur in the tree.
+ *
+ * <p>If a field descriptor which does not, or cannot occur in the proto structure is supplied, it
+ * is silently ignored.
+ */
+ public IterableOfProtosFluentAssertion<M> ignoringFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(config.ignoringFieldDescriptors(fieldDescriptors));
+ }
+
+ /**
+ * Excludes all specific field paths under the argument {@link FieldScope} from the comparison.
+ *
+ * <p>This method is additive and has well-defined ordering semantics. If the invoking {@link
+ * ProtoFluentAssertion} is already scoped to a {@link FieldScope} {@code X}, and this method is
+ * invoked with {@link FieldScope} {@code Y}, the resultant {@link ProtoFluentAssertion} is
+ * constrained to the subtraction of {@code X - Y}.
+ *
+ * <p>By default, {@link ProtoFluentAssertion} is constrained to {@link FieldScopes#all()}, that
+ * is, no fields are excluded from comparison.
+ */
+ public IterableOfProtosFluentAssertion<M> ignoringFieldScope(FieldScope fieldScope) {
+ return usingConfig(config.ignoringFieldScope(checkNotNull(fieldScope, "fieldScope")));
+ }
+
+ /**
+ * If set, in the event of a comparison failure, the error message printed will list only those
+ * specific fields that did not match between the actual and expected values. Useful for very
+ * large protocol buffers.
+ *
+ * <p>This a purely cosmetic setting, and it has no effect on the behavior of the test.
+ */
+ public IterableOfProtosFluentAssertion<M> reportingMismatchesOnly() {
+ return usingConfig(config.reportingMismatchesOnly());
+ }
+
+ /**
+ * Specifies the {@link TypeRegistry} and {@link ExtensionRegistry} to use for {@link
+ * com.google.protobuf.Any Any} messages.
+ *
+ * <p>To compare the value of an {@code Any} message, ProtoTruth looks in the given type registry
+ * for a descriptor for the message's type URL:
+ *
+ * <ul>
+ * <li>If ProtoTruth finds a descriptor, it unpacks the value and compares it against the
+ * expected value, respecting any configuration methods used for the assertion.
+ * <li>If ProtoTruth does not find a descriptor (or if the value can't be deserialized with the
+ * descriptor), it compares the raw, serialized bytes of the expected and actual values.
+ * </ul>
+ *
+ * <p>When ProtoTruth unpacks a value, it is parsing a serialized proto. That proto may contain
+ * extensions. To look up those extensions, ProtoTruth uses the provided {@link
+ * ExtensionRegistry}.
+ *
+ * @since 1.1
+ */
+ public IterableOfProtosFluentAssertion<M> unpackingAnyUsing(
+ TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry) {
+ return usingConfig(config.unpackingAnyUsing(typeRegistry, extensionRegistry));
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // Overrides for IterableSubject Methods
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * @deprecated Protos do not implement {@link Comparable}, so you must {@linkplain
+ * #isInStrictOrder(Comparator) supply a comparator}.
+ * @throws ClassCastException always
+ */
+ @Override
+ @Deprecated
+ public final void isInStrictOrder() {
+ throw new ClassCastException(
+ "Protos do not implement Comparable, so you must supply a Comparator.");
+ }
+
+ /**
+ * @deprecated Protos do not implement {@link Comparable}, so you must {@linkplain
+ * #isInOrder(Comparator) supply a comparator}.
+ * @throws ClassCastException always
+ */
+ @Override
+ @Deprecated
+ public final void isInOrder() {
+ throw new ClassCastException(
+ "Protos do not implement Comparable, so you must supply a Comparator.");
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // UsingCorrespondence Methods
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+
+ // A forwarding implementation of IterableSubject.UsingCorrespondence which passes the expected
+ // protos to FluentEqualityConfig before comparing. This is required to support
+ // displayingDiffsPairedBy(), since we can't pass the user to a vanilla
+ // IterableSubject.UsingCorrespondence until we know what the expected messages are.
+ private static class UsingCorrespondence<M extends Message>
+ implements IterableOfProtosUsingCorrespondence<M> {
+ private final IterableOfProtosSubject<M> subject;
+ private final @Nullable Function<? super M, ? extends Object> keyFunction;
+
+ UsingCorrespondence(
+ IterableOfProtosSubject<M> subject,
+ @Nullable Function<? super M, ? extends Object> keyFunction) {
+ this.subject = checkNotNull(subject);
+ this.keyFunction = keyFunction;
+ }
+
+ private IterableSubject.UsingCorrespondence<M, M> delegate(Iterable<? extends M> messages) {
+ IterableSubject.UsingCorrespondence<M, M> usingCorrespondence =
+ subject.comparingElementsUsing(
+ subject
+ .config
+ .withExpectedMessages(messages)
+ .<M>toCorrespondence(FieldScopeUtil.getSingleDescriptor(subject.actual)));
+ if (keyFunction != null) {
+ usingCorrespondence = usingCorrespondence.displayingDiffsPairedBy(keyFunction);
+ }
+ return usingCorrespondence;
+ }
+
+ @Override
+ public IterableOfProtosUsingCorrespondence<M> displayingDiffsPairedBy(
+ Function<? super M, ?> keyFunction) {
+ return new UsingCorrespondence<M>(subject, checkNotNull(keyFunction));
+ }
+
+ @Override
+ public void contains(@Nullable M expected) {
+ delegate(Arrays.asList(expected)).contains(expected);
+ }
+
+ @Override
+ public void doesNotContain(@Nullable M excluded) {
+ delegate(Arrays.asList(excluded)).doesNotContain(excluded);
+ }
+
+ @Override
+ @CanIgnoreReturnValue
+ public Ordered containsExactly(/*@Nullable*/ M... expected) {
+ return delegate(Arrays.asList(expected)).containsExactly(expected);
+ }
+
+ @Override
+ @CanIgnoreReturnValue
+ public Ordered containsExactlyElementsIn(Iterable<? extends M> expected) {
+ return delegate(expected).containsExactlyElementsIn(expected);
+ }
+
+ @Override
+ @CanIgnoreReturnValue
+ public Ordered containsExactlyElementsIn(M[] expected) {
+ return delegate(Arrays.asList(expected)).containsExactlyElementsIn(expected);
+ }
+
+ @Override
+ @CanIgnoreReturnValue
+ public Ordered containsAtLeast(@Nullable M first, @Nullable M second, /*@Nullable*/ M... rest) {
+ return delegate(Lists.asList(first, second, rest)).containsAtLeast(first, second, rest);
+ }
+
+ @Override
+ @CanIgnoreReturnValue
+ public Ordered containsAtLeastElementsIn(Iterable<? extends M> expected) {
+ return delegate(expected).containsAtLeastElementsIn(expected);
+ }
+
+ @Override
+ @CanIgnoreReturnValue
+ public Ordered containsAtLeastElementsIn(M[] expected) {
+ return delegate(Arrays.asList(expected)).containsAtLeastElementsIn(expected);
+ }
+
+ @Override
+ public void containsAnyOf(@Nullable M first, @Nullable M second, /*@Nullable*/ M... rest) {
+ delegate(Lists.asList(first, second, rest)).containsAnyOf(first, second, rest);
+ }
+
+ @Override
+ public void containsAnyIn(Iterable<? extends M> expected) {
+ delegate(expected).containsAnyIn(expected);
+ }
+
+ @Override
+ public void containsAnyIn(M[] expected) {
+ delegate(Arrays.asList(expected)).containsAnyIn(expected);
+ }
+
+ @Override
+ public void containsNoneOf(
+ @Nullable M firstExcluded, @Nullable M secondExcluded, /*@Nullable*/ M... restOfExcluded) {
+ delegate(Lists.asList(firstExcluded, secondExcluded, restOfExcluded))
+ .containsNoneOf(firstExcluded, secondExcluded, restOfExcluded);
+ }
+
+ @Override
+ public void containsNoneIn(Iterable<? extends M> excluded) {
+ delegate(excluded).containsNoneIn(excluded);
+ }
+
+ @Override
+ public void containsNoneIn(M[] excluded) {
+ delegate(Arrays.asList(excluded)).containsNoneIn(excluded);
+ }
+ }
+
+ private IterableOfProtosUsingCorrespondence<M> usingCorrespondence() {
+ return new UsingCorrespondence<M>(this, /* keyFunction= */ null);
+ }
+
+ // The UsingCorrespondence methods have conflicting erasure with default IterableSubject methods,
+ // so we can't implement them both on the same class, but we want to define both so
+ // IterableOfProtosSubjects are interchangeable with IterableSubjects when no configuration is
+ // specified. So, we implement a dumb, private delegator to return instead.
+ private static final class IterableOfProtosFluentAssertionImpl<M extends Message>
+ implements IterableOfProtosFluentAssertion<M> {
+ private final IterableOfProtosSubject<M> subject;
+
+ IterableOfProtosFluentAssertionImpl(IterableOfProtosSubject<M> subject) {
+ this.subject = subject;
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> ignoringFieldAbsence() {
+ return subject.ignoringFieldAbsence();
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> ignoringFieldAbsenceOfFields(
+ int firstFieldNumber, int... rest) {
+ return subject.ignoringFieldAbsenceOfFields(firstFieldNumber, rest);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> ignoringFieldAbsenceOfFields(
+ Iterable<Integer> fieldNumbers) {
+ return subject.ignoringFieldAbsenceOfFields(fieldNumbers);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> ignoringFieldAbsenceOfFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return subject.ignoringFieldAbsenceOfFieldDescriptors(firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> ignoringFieldAbsenceOfFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return subject.ignoringFieldAbsenceOfFieldDescriptors(fieldDescriptors);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> ignoringRepeatedFieldOrder() {
+ return subject.ignoringRepeatedFieldOrder();
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> ignoringRepeatedFieldOrderOfFields(
+ int firstFieldNumber, int... rest) {
+ return subject.ignoringRepeatedFieldOrderOfFields(firstFieldNumber, rest);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> ignoringRepeatedFieldOrderOfFields(
+ Iterable<Integer> fieldNumbers) {
+ return subject.ignoringRepeatedFieldOrderOfFields(fieldNumbers);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return subject.ignoringRepeatedFieldOrderOfFieldDescriptors(firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return subject.ignoringRepeatedFieldOrderOfFieldDescriptors(fieldDescriptors);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> ignoringExtraRepeatedFieldElements() {
+ return subject.ignoringExtraRepeatedFieldElements();
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> ignoringExtraRepeatedFieldElementsOfFields(
+ int firstFieldNumber, int... rest) {
+ return subject.ignoringExtraRepeatedFieldElementsOfFields(firstFieldNumber, rest);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> ignoringExtraRepeatedFieldElementsOfFields(
+ Iterable<Integer> fieldNumbers) {
+ return subject.ignoringExtraRepeatedFieldElementsOfFields(fieldNumbers);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> ignoringExtraRepeatedFieldElementsOfFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return subject.ignoringExtraRepeatedFieldElementsOfFieldDescriptors(
+ firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> ignoringExtraRepeatedFieldElementsOfFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return subject.ignoringExtraRepeatedFieldElementsOfFieldDescriptors(fieldDescriptors);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> usingDoubleTolerance(double tolerance) {
+ return subject.usingDoubleTolerance(tolerance);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> usingDoubleToleranceForFields(
+ double tolerance, int firstFieldNumber, int... rest) {
+ return subject.usingDoubleToleranceForFields(tolerance, firstFieldNumber, rest);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> usingDoubleToleranceForFields(
+ double tolerance, Iterable<Integer> fieldNumbers) {
+ return subject.usingDoubleToleranceForFields(tolerance, fieldNumbers);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> usingDoubleToleranceForFieldDescriptors(
+ double tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return subject.usingDoubleToleranceForFieldDescriptors(tolerance, firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> usingDoubleToleranceForFieldDescriptors(
+ double tolerance, Iterable<FieldDescriptor> fieldDescriptors) {
+ return subject.usingDoubleToleranceForFieldDescriptors(tolerance, fieldDescriptors);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> usingFloatTolerance(float tolerance) {
+ return subject.usingFloatTolerance(tolerance);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> usingFloatToleranceForFields(
+ float tolerance, int firstFieldNumber, int... rest) {
+ return subject.usingFloatToleranceForFields(tolerance, firstFieldNumber, rest);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> usingFloatToleranceForFields(
+ float tolerance, Iterable<Integer> fieldNumbers) {
+ return subject.usingFloatToleranceForFields(tolerance, fieldNumbers);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> usingFloatToleranceForFieldDescriptors(
+ float tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return subject.usingFloatToleranceForFieldDescriptors(tolerance, firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> usingFloatToleranceForFieldDescriptors(
+ float tolerance, Iterable<FieldDescriptor> fieldDescriptors) {
+ return subject.usingFloatToleranceForFieldDescriptors(tolerance, fieldDescriptors);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> comparingExpectedFieldsOnly() {
+ return subject.comparingExpectedFieldsOnly();
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> withPartialScope(FieldScope fieldScope) {
+ return subject.withPartialScope(fieldScope);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> ignoringFields(int firstFieldNumber, int... rest) {
+ return subject.ignoringFields(firstFieldNumber, rest);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> ignoringFields(Iterable<Integer> fieldNumbers) {
+ return subject.ignoringFields(fieldNumbers);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> ignoringFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return subject.ignoringFieldDescriptors(firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> ignoringFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return subject.ignoringFieldDescriptors(fieldDescriptors);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> ignoringFieldScope(FieldScope fieldScope) {
+ return subject.ignoringFieldScope(fieldScope);
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> reportingMismatchesOnly() {
+ return subject.reportingMismatchesOnly();
+ }
+
+ @Override
+ public IterableOfProtosFluentAssertion<M> unpackingAnyUsing(
+ TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry) {
+ return subject.unpackingAnyUsing(typeRegistry, extensionRegistry);
+ }
+
+ @Override
+ public IterableOfProtosUsingCorrespondence<M> displayingDiffsPairedBy(
+ Function<? super M, ?> keyFunction) {
+ return usingCorrespondence().displayingDiffsPairedBy(keyFunction);
+ }
+
+ @Override
+ public void contains(@Nullable M expected) {
+ usingCorrespondence().contains(expected);
+ }
+
+ @Override
+ public void doesNotContain(@Nullable M excluded) {
+ usingCorrespondence().doesNotContain(excluded);
+ }
+
+ @Override
+ public Ordered containsExactly(/*@Nullable*/ M... expected) {
+ return usingCorrespondence().containsExactly(expected);
+ }
+
+ @Override
+ public Ordered containsExactlyElementsIn(Iterable<? extends M> expected) {
+ return usingCorrespondence().containsExactlyElementsIn(expected);
+ }
+
+ @Override
+ public Ordered containsExactlyElementsIn(M[] expected) {
+ return usingCorrespondence().containsExactlyElementsIn(expected);
+ }
+
+ @Override
+ public Ordered containsAtLeast(@Nullable M first, @Nullable M second, /*@Nullable*/ M... rest) {
+ return usingCorrespondence().containsAtLeast(first, second, rest);
+ }
+
+ @Override
+ public Ordered containsAtLeastElementsIn(Iterable<? extends M> expected) {
+ return usingCorrespondence().containsAtLeastElementsIn(expected);
+ }
+
+ @Override
+ public Ordered containsAtLeastElementsIn(M[] expected) {
+ return usingCorrespondence().containsAtLeastElementsIn(expected);
+ }
+
+ @Override
+ public void containsAnyOf(@Nullable M first, @Nullable M second, /*@Nullable*/ M... rest) {
+ usingCorrespondence().containsAnyOf(first, second, rest);
+ }
+
+ @Override
+ public void containsAnyIn(Iterable<? extends M> expected) {
+ usingCorrespondence().containsAnyIn(expected);
+ }
+
+ @Override
+ public void containsAnyIn(M[] expected) {
+ usingCorrespondence().containsAnyIn(expected);
+ }
+
+ @Override
+ public void containsNoneOf(
+ @Nullable M firstExcluded, @Nullable M secondExcluded, /*@Nullable*/ M... restOfExcluded) {
+ usingCorrespondence().containsNoneOf(firstExcluded, secondExcluded, restOfExcluded);
+ }
+
+ @Override
+ public void containsNoneIn(Iterable<? extends M> excluded) {
+ usingCorrespondence().containsNoneIn(excluded);
+ }
+
+ @Override
+ public void containsNoneIn(M[] excluded) {
+ usingCorrespondence().containsNoneIn(excluded);
+ }
+
+ @SuppressWarnings("DoNotCall")
+ @Override
+ @Deprecated
+ public boolean equals(Object o) {
+ return subject.equals(o);
+ }
+
+ @SuppressWarnings("DoNotCall")
+ @Override
+ @Deprecated
+ public int hashCode() {
+ return subject.hashCode();
+ }
+
+ private final IterableOfProtosUsingCorrespondence<M> usingCorrespondence() {
+ return subject.usingCorrespondence();
+ }
+ }
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/IterableOfProtosUsingCorrespondence.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/IterableOfProtosUsingCorrespondence.java
new file mode 100644
index 00000000..5a49369a
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/IterableOfProtosUsingCorrespondence.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import com.google.common.base.Function;
+import com.google.common.truth.Ordered;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.protobuf.Message;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Comparison methods, which enforce the rules set in prior calls to {@link
+ * IterableOfProtosFluentAssertion}.
+ */
+public interface IterableOfProtosUsingCorrespondence<M extends Message> {
+
+ /**
+ * Specifies a way to pair up unexpected and missing elements in the message when an assertion
+ * fails. For example:
+ *
+ * <pre>{@code
+ * assertThat(actualFoos)
+ * .ignoringRepeatedFieldOrder()
+ * .ignoringFields(Foo.BAR_FIELD_NUMBER)
+ * .displayingDiffsPairedBy(Foo::getId)
+ * .containsExactlyElementsIn(expectedFoos);
+ * }</pre>
+ *
+ * <p>On assertions where it makes sense to do so, the elements are paired as follows: they are
+ * keyed by {@code keyFunction}, and if an unexpected element and a missing element have the same
+ * non-null key then the they are paired up. (Elements with null keys are not paired.) The failure
+ * message will show paired elements together, and a diff will be shown.
+ *
+ * <p>The expected elements given in the assertion should be uniquely keyed by {@code
+ * keyFunction}. If multiple missing elements have the same key then the pairing will be skipped.
+ *
+ * <p>Useful key functions will have the property that key equality is less strict than the
+ * already specified equality rules; i.e. given {@code actual} and {@code expected} values with
+ * keys {@code actualKey} and {@code expectedKey}, if {@code actual} and {@code expected} compare
+ * equal given the rest of the directives such as {@code ignoringRepeatedFieldOrder} and {@code
+ * ignoringFields}, then it is guaranteed that {@code actualKey} is equal to {@code expectedKey},
+ * but there are cases where {@code actualKey} is equal to {@code expectedKey} but the direct
+ * comparison fails.
+ *
+ * <p>Note that calling this method makes no difference to whether a test passes or fails, it just
+ * improves the message if it fails.
+ */
+ IterableOfProtosUsingCorrespondence<M> displayingDiffsPairedBy(
+ Function<? super M, ?> keyFunction);
+
+ /**
+ * Checks that the subject contains at least one element that corresponds to the given expected
+ * element.
+ */
+ void contains(@Nullable M expected);
+
+ /** Checks that none of the actual elements correspond to the given element. */
+ void doesNotContain(@Nullable M excluded);
+
+ /**
+ * Checks that subject contains exactly elements that correspond to the expected elements, i.e.
+ * that there is a 1:1 mapping between the actual elements and the expected elements where each
+ * pair of elements correspond.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method.
+ *
+ * <p>To test that the iterable contains the same elements as an array, prefer {@link
+ * #containsExactlyElementsIn(Message[])}. It makes clear that the given array is a list of
+ * elements, not an element itself.
+ */
+ @CanIgnoreReturnValue
+ Ordered containsExactly(/*@Nullable*/ M... expected);
+
+ /**
+ * Checks that subject contains exactly elements that correspond to the expected elements, i.e.
+ * that there is a 1:1 mapping between the actual elements and the expected elements where each
+ * pair of elements correspond.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method.
+ */
+ @CanIgnoreReturnValue
+ Ordered containsExactlyElementsIn(Iterable<? extends M> expected);
+
+ /**
+ * Checks that subject contains exactly elements that correspond to the expected elements, i.e.
+ * that there is a 1:1 mapping between the actual elements and the expected elements where each
+ * pair of elements correspond.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method.
+ */
+ @CanIgnoreReturnValue
+ Ordered containsExactlyElementsIn(M[] expected);
+
+ /**
+ * Checks that the subject contains elements that corresponds to all of the expected elements,
+ * i.e. that there is a 1:1 mapping between any subset of the actual elements and the expected
+ * elements where each pair of elements correspond.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method. The elements must appear in the given order within the
+ * subject, but they are not required to be consecutive.
+ */
+ @CanIgnoreReturnValue
+ Ordered containsAtLeast(@Nullable M first, @Nullable M second, /*@Nullable*/ M... rest);
+
+ /**
+ * Checks that the subject contains elements that corresponds to all of the expected elements,
+ * i.e. that there is a 1:1 mapping between any subset of the actual elements and the expected
+ * elements where each pair of elements correspond.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method. The elements must appear in the given order within the
+ * subject, but they are not required to be consecutive.
+ */
+ @CanIgnoreReturnValue
+ Ordered containsAtLeastElementsIn(Iterable<? extends M> expected);
+
+ /**
+ * Checks that the subject contains elements that corresponds to all of the expected elements,
+ * i.e. that there is a 1:1 mapping between any subset of the actual elements and the expected
+ * elements where each pair of elements correspond.
+ *
+ * <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
+ * on the object returned by this method. The elements must appear in the given order within the
+ * subject, but they are not required to be consecutive.
+ */
+ @CanIgnoreReturnValue
+ Ordered containsAtLeastElementsIn(M[] expected);
+
+ /**
+ * Checks that the subject contains at least one element that corresponds to at least one of the
+ * expected elements.
+ */
+ void containsAnyOf(@Nullable M first, @Nullable M second, /*@Nullable*/ M... rest);
+
+ /**
+ * Checks that the subject contains at least one element that corresponds to at least one of the
+ * expected elements.
+ */
+ void containsAnyIn(Iterable<? extends M> expected);
+
+ /**
+ * Checks that the subject contains at least one element that corresponds to at least one of the
+ * expected elements.
+ */
+ void containsAnyIn(M[] expected);
+
+ /**
+ * Checks that the subject contains no elements that correspond to any of the given elements.
+ * (Duplicates are irrelevant to this test, which fails if any of the subject elements correspond
+ * to any of the given elements.)
+ */
+ void containsNoneOf(
+ @Nullable M firstExcluded, @Nullable M secondExcluded, /*@Nullable*/ M... restOfExcluded);
+
+ /**
+ * Checks that the subject contains no elements that correspond to any of the given elements.
+ * (Duplicates are irrelevant to this test, which fails if any of the subject elements correspond
+ * to any of the given elements.)
+ */
+ void containsNoneIn(Iterable<? extends M> excluded);
+
+ /**
+ * Checks that the subject contains no elements that correspond to any of the given elements.
+ * (Duplicates are irrelevant to this test, which fails if any of the subject elements correspond
+ * to any of the given elements.)
+ */
+ void containsNoneIn(M[] excluded);
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/MapWithProtoValuesFluentAssertion.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/MapWithProtoValuesFluentAssertion.java
new file mode 100644
index 00000000..fee52423
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/MapWithProtoValuesFluentAssertion.java
@@ -0,0 +1,551 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import com.google.common.truth.Ordered;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.ExtensionRegistry;
+import com.google.protobuf.Message;
+import com.google.protobuf.TypeRegistry;
+import java.util.Map;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Fluent API to perform detailed, customizable comparison of maps containing protocol buffers as
+ * values. The same comparison rules are applied to all pairs of protocol buffers which get
+ * compared.
+ *
+ * <p>The <b>keys</b> of these maps are treated as ordinary objects, and keys which happen to be
+ * protocol buffers are not given special treatment. They are compared with {@link Object#equals}
+ * and {@link Object#hashCode()} as documented by the {@link java.util.Map} interface.
+ *
+ * <p>Methods may be chained in any order, but the chain should terminate with a method that doesn't
+ * return a {@code MapWithProtoValuesFluentAssertion}, such as {@link #containsExactly} or {@link
+ * #containsEntry}.
+ *
+ * <p>The state of a {@code MapWithProtoValuesFluentAssertion} object after each method is called is
+ * left undefined. Users should not retain references to {@code MapWithProtoValuesFluentAssertion}
+ * instances.
+ */
+public interface MapWithProtoValuesFluentAssertion<M extends Message> {
+
+ /**
+ * Specifies that the 'has' bit of individual fields should be ignored when comparing for
+ * equality.
+ *
+ * <p>For version 2 Protocol Buffers, this setting determines whether two protos with the same
+ * value for a field compare equal if one explicitly sets the value, and the other merely
+ * implicitly uses the schema-defined default. This setting also determines whether unknown fields
+ * should be considered in the comparison. By {@code ignoringFieldAbsence()}, unknown fields are
+ * ignored, and value-equal fields as specified above are considered equal.
+ *
+ * <p>For version 3 Protocol Buffers, this setting does not affect primitive fields, because their
+ * default value is indistinguishable from unset.
+ */
+ MapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceForValues();
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified top-level field numbers should be
+ * ignored when comparing for equality. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if they are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsenceForValues()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsenceForValues() for details
+ */
+ MapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceOfFieldsForValues(
+ int firstFieldNumber, int... rest);
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified top-level field numbers should be
+ * ignored when comparing for equality. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if they are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsenceForValues()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsenceForValues() for details
+ */
+ MapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceOfFieldsForValues(
+ Iterable<Integer> fieldNumbers);
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified field descriptors should be ignored
+ * when comparing for equality. Sub-fields must be specified explicitly if they are to be ignored
+ * as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsenceForValues()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsenceForValues() for details
+ */
+ MapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceOfFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified field descriptors should be ignored
+ * when comparing for equality. Sub-fields must be specified explicitly if they are to be ignored
+ * as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsenceForValues()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsenceForValues() for details
+ */
+ MapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceOfFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Specifies that the ordering of repeated fields, at all levels, should be ignored when comparing
+ * for equality.
+ *
+ * <p>This setting applies to all repeated fields recursively, but it does not ignore structure.
+ * For example, with {@link #ignoringRepeatedFieldOrderForValues()}, a repeated {@code int32}
+ * field {@code bar}, set inside a repeated message field {@code foo}, the following protos will
+ * all compare equal:
+ *
+ * <pre>{@code
+ * message1: {
+ * foo: {
+ * bar: 1
+ * bar: 2
+ * }
+ * foo: {
+ * bar: 3
+ * bar: 4
+ * }
+ * }
+ *
+ * message2: {
+ * foo: {
+ * bar: 2
+ * bar: 1
+ * }
+ * foo: {
+ * bar: 4
+ * bar: 3
+ * }
+ * }
+ *
+ * message3: {
+ * foo: {
+ * bar: 4
+ * bar: 3
+ * }
+ * foo: {
+ * bar: 2
+ * bar: 1
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>However, the following message will compare equal to none of these:
+ *
+ * <pre>{@code
+ * message4: {
+ * foo: {
+ * bar: 1
+ * bar: 3
+ * }
+ * foo: {
+ * bar: 2
+ * bar: 4
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>This setting does not apply to map fields, for which field order is always ignored. The
+ * serialization order of map fields is undefined, and it may change from runtime to runtime.
+ */
+ MapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderForValues();
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified top-level field
+ * numbers should be ignored when comparing for equality. Sub-fields must be specified explicitly
+ * (via {@link FieldDescriptor}) if their orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrderForValues()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrderForValues() for details.
+ */
+ MapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldsForValues(
+ int firstFieldNumber, int... rest);
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified top-level field
+ * numbers should be ignored when comparing for equality. Sub-fields must be specified explicitly
+ * (via {@link FieldDescriptor}) if their orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrderForValues()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrderForValues() for details.
+ */
+ MapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldsForValues(
+ Iterable<Integer> fieldNumbers);
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified field descriptors
+ * should be ignored when comparing for equality. Sub-fields must be specified explicitly if their
+ * orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrderForValues()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrderForValues() for details.
+ */
+ MapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified field descriptors
+ * should be ignored when comparing for equality. Sub-fields must be specified explicitly if their
+ * orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrderForValues()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrderForValues() for details.
+ */
+ MapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Specifies that, for all repeated and map fields, any elements in the 'actual' proto which are
+ * not found in the 'expected' proto are ignored, with the exception of fields in the expected
+ * proto which are empty. To ignore empty repeated fields as well, use {@link
+ * #comparingExpectedFieldsOnlyForValues}.
+ *
+ * <p>This rule is applied independently from {@link #ignoringRepeatedFieldOrderForValues}. If
+ * ignoring repeated field order AND extra repeated field elements, all that is tested is that the
+ * expected elements comprise a subset of the actual elements. If not ignoring repeated field
+ * order, but still ignoring extra repeated field elements, the actual elements must contain a
+ * subsequence that matches the expected elements for the test to pass. (The subsequence rule does
+ * not apply to Map fields, which are always compared by key.)
+ */
+ MapWithProtoValuesFluentAssertion<M> ignoringExtraRepeatedFieldElementsForValues();
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified top-level field
+ * numbers should be ignored. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if their extra elements are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElementsForValues()} instead to ignore these for all
+ * fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElementsForValues() for details.
+ */
+ MapWithProtoValuesFluentAssertion<M> ignoringExtraRepeatedFieldElementsOfFieldsForValues(
+ int firstFieldNumber, int... rest);
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified top-level field
+ * numbers should be ignored. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if their extra elements are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElementsForValues()} instead to ignore these for all
+ * fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElementsForValues() for details.
+ */
+ MapWithProtoValuesFluentAssertion<M> ignoringExtraRepeatedFieldElementsOfFieldsForValues(
+ Iterable<Integer> fieldNumbers);
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified field descriptors
+ * should be ignored. Sub-fields must be specified explicitly if their extra elements are to be
+ * ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElementsForValues()} instead to ignore these for all
+ * fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElementsForValues() for details.
+ */
+ MapWithProtoValuesFluentAssertion<M>
+ ignoringExtraRepeatedFieldElementsOfFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified field descriptors
+ * should be ignored. Sub-fields must be specified explicitly if their extra elements are to be
+ * ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElementsForValues()} instead to ignore these for all
+ * fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElementsForValues() for details.
+ */
+ MapWithProtoValuesFluentAssertion<M>
+ ignoringExtraRepeatedFieldElementsOfFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Compares double fields as equal if they are both finite and their absolute difference is less
+ * than or equal to {@code tolerance}.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ MapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForValues(double tolerance);
+
+ /**
+ * Compares double fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ MapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForFieldsForValues(
+ double tolerance, int firstFieldNumber, int... rest);
+
+ /**
+ * Compares double fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ MapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForFieldsForValues(
+ double tolerance, Iterable<Integer> fieldNumbers);
+
+ /**
+ * Compares double fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ MapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForFieldDescriptorsForValues(
+ double tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Compares double fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ MapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForFieldDescriptorsForValues(
+ double tolerance, Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Compares float fields as equal if they are both finite and their absolute difference is less
+ * than or equal to {@code tolerance}.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ MapWithProtoValuesFluentAssertion<M> usingFloatToleranceForValues(float tolerance);
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ MapWithProtoValuesFluentAssertion<M> usingFloatToleranceForFieldsForValues(
+ float tolerance, int firstFieldNumber, int... rest);
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ MapWithProtoValuesFluentAssertion<M> usingFloatToleranceForFieldsForValues(
+ float tolerance, Iterable<Integer> fieldNumbers);
+
+ /**
+ * Compares float fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ MapWithProtoValuesFluentAssertion<M> usingFloatToleranceForFieldDescriptorsForValues(
+ float tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ MapWithProtoValuesFluentAssertion<M> usingFloatToleranceForFieldDescriptorsForValues(
+ float tolerance, Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Limits the comparison of Protocol buffers to the fields set in the expected proto(s). When
+ * multiple protos are specified, the comparison is limited to the union of set fields in all the
+ * expected protos.
+ *
+ * <p>The "expected proto(s)" are those passed to the method at the end of the call chain, such as
+ * {@link #containsEntry} or {@link #containsExactlyEntriesIn}.
+ *
+ * <p>Fields not set in the expected proto(s) are ignored. In particular, proto3 fields which have
+ * their default values are ignored, as these are indistinguishable from unset fields. If you want
+ * to assert that a proto3 message has certain fields with default values, you cannot use this
+ * method.
+ */
+ MapWithProtoValuesFluentAssertion<M> comparingExpectedFieldsOnlyForValues();
+
+ /**
+ * Limits the comparison of Protocol buffers to the defined {@link FieldScope}.
+ *
+ * <p>This method is additive and has well-defined ordering semantics. If the invoking {@link
+ * MapWithProtoValuesFluentAssertion} is already scoped to a {@link FieldScope} {@code X}, and
+ * this method is invoked with {@link FieldScope} {@code Y}, the resultant {@link
+ * MapWithProtoValuesFluentAssertion} is constrained to the intersection of {@link FieldScope}s
+ * {@code X} and {@code Y}.
+ *
+ * <p>By default, {@link MapWithProtoValuesFluentAssertion} is constrained to {@link
+ * FieldScopes#all()}, that is, no fields are excluded from comparison.
+ */
+ MapWithProtoValuesFluentAssertion<M> withPartialScopeForValues(FieldScope fieldScope);
+
+ /**
+ * Excludes the top-level message fields with the given tag numbers from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * numbers are ignored, and all sub-messages of type {@code M} will also have these field numbers
+ * ignored.
+ *
+ * <p>If an invalid field number is supplied, the terminal comparison operation will throw a
+ * runtime exception.
+ */
+ MapWithProtoValuesFluentAssertion<M> ignoringFieldsForValues(int firstFieldNumber, int... rest);
+
+ /**
+ * Excludes the top-level message fields with the given tag numbers from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * numbers are ignored, and all sub-messages of type {@code M} will also have these field numbers
+ * ignored.
+ *
+ * <p>If an invalid field number is supplied, the terminal comparison operation will throw a
+ * runtime exception.
+ */
+ MapWithProtoValuesFluentAssertion<M> ignoringFieldsForValues(Iterable<Integer> fieldNumbers);
+
+ /**
+ * Excludes all message fields matching the given {@link FieldDescriptor}s from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * descriptors are ignored, no matter where they occur in the tree.
+ *
+ * <p>If a field descriptor which does not, or cannot occur in the proto structure is supplied, it
+ * is silently ignored.
+ */
+ MapWithProtoValuesFluentAssertion<M> ignoringFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Excludes all message fields matching the given {@link FieldDescriptor}s from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * descriptors are ignored, no matter where they occur in the tree.
+ *
+ * <p>If a field descriptor which does not, or cannot occur in the proto structure is supplied, it
+ * is silently ignored.
+ */
+ MapWithProtoValuesFluentAssertion<M> ignoringFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Excludes all specific field paths under the argument {@link FieldScope} from the comparison.
+ *
+ * <p>This method is additive and has well-defined ordering semantics. If the invoking {@link
+ * MapWithProtoValuesFluentAssertion} is already scoped to a {@link FieldScope} {@code X}, and
+ * this method is invoked with {@link FieldScope} {@code Y}, the resultant {@link
+ * MapWithProtoValuesFluentAssertion} is constrained to the subtraction of {@code X - Y}.
+ *
+ * <p>By default, {@link MapWithProtoValuesFluentAssertion} is constrained to {@link
+ * FieldScopes#all()}, that is, no fields are excluded from comparison.
+ */
+ MapWithProtoValuesFluentAssertion<M> ignoringFieldScopeForValues(FieldScope fieldScope);
+
+ /**
+ * If set, in the event of a comparison failure, the error message printed will list only those
+ * specific fields that did not match between the actual and expected values. Useful for very
+ * large protocol buffers.
+ *
+ * <p>This a purely cosmetic setting, and it has no effect on the behavior of the test.
+ */
+ MapWithProtoValuesFluentAssertion<M> reportingMismatchesOnlyForValues();
+
+ /**
+ * Specifies the {@link TypeRegistry} and {@link ExtensionRegistry} to use for {@link
+ * com.google.protobuf.Any Any} messages.
+ *
+ * <p>To compare the value of an {@code Any} message, ProtoTruth looks in the given type registry
+ * for a descriptor for the message's type URL:
+ *
+ * <ul>
+ * <li>If ProtoTruth finds a descriptor, it unpacks the value and compares it against the
+ * expected value, respecting any configuration methods used for the assertion.
+ * <li>If ProtoTruth does not find a descriptor (or if the value can't be deserialized with the
+ * descriptor), it compares the raw, serialized bytes of the expected and actual values.
+ * </ul>
+ *
+ * <p>When ProtoTruth unpacks a value, it is parsing a serialized proto. That proto may contain
+ * extensions. To look up those extensions, ProtoTruth uses the provided {@link
+ * ExtensionRegistry}.
+ *
+ * @since 1.1
+ */
+ MapWithProtoValuesFluentAssertion<M> unpackingAnyUsingForValues(
+ TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry);
+
+ /**
+ * Fails if the map does not contain an entry with the given key and a value that corresponds to
+ * the given value.
+ */
+ void containsEntry(@Nullable Object expectedKey, @Nullable M expectedValue);
+
+ /**
+ * Fails if the map contains an entry with the given key and a value that corresponds to the given
+ * value.
+ */
+ void doesNotContainEntry(@Nullable Object excludedKey, @Nullable M excludedValue);
+
+ /**
+ * Fails if the map does not contain exactly the given set of keys mapping to values that
+ * correspond to the given values.
+ *
+ * <p>The values must all be of type {@code M}, and a {@link ClassCastException} will be thrown if
+ * any other type is encountered.
+ *
+ * <p><b>Warning:</b> the use of varargs means that we cannot guarantee an equal number of
+ * key/value pairs at compile time. Please make sure you provide varargs in key/value pairs!
+ */
+ @CanIgnoreReturnValue
+ Ordered containsExactly(@Nullable Object k0, @Nullable M v0, /*@Nullable*/ Object... rest);
+
+ /**
+ * Fails if the map does not contain exactly the keys in the given map, mapping to values that
+ * correspond to the values of the given map.
+ */
+ @CanIgnoreReturnValue
+ Ordered containsExactlyEntriesIn(Map<?, ? extends M> expectedMap);
+
+ /**
+ * @deprecated Do not call {@code equals()} on a {@code MapWithProtoValuesFluentAssertion}.
+ * @see com.google.common.truth.Subject#equals(Object)
+ */
+ @Override
+ @Deprecated
+ boolean equals(Object o);
+
+ /**
+ * @deprecated {@code MapWithProtoValuesFluentAssertion} does not support {@code hashCode()}.
+ * @see com.google.common.truth.Subject#hashCode()
+ */
+ @Override
+ @Deprecated
+ int hashCode();
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/MapWithProtoValuesSubject.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/MapWithProtoValuesSubject.java
new file mode 100644
index 00000000..2f4fac81
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/MapWithProtoValuesSubject.java
@@ -0,0 +1,905 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.Lists.asList;
+import static com.google.common.truth.extensions.proto.FieldScopeUtil.asList;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.MapSubject;
+import com.google.common.truth.Ordered;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.ExtensionRegistry;
+import com.google.protobuf.Message;
+import com.google.protobuf.TypeRegistry;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Truth subject for maps with protocol buffers for values.
+ *
+ * <p>{@code ProtoTruth.assertThat(actual).containsExactlyEntriesIn(expected)} performs the same
+ * assertion as {@code Truth.assertThat(actual).containsExactlyEntriesIn(expected)}. By default, the
+ * assertions are strict with respect to repeated field order, missing fields, etc. This behavior
+ * can be changed with the configuration methods on this subject, e.g. {@code
+ * ProtoTruth.assertThat(actual).ignoringRepeatedFieldOrder().containsExactly(expected)}.
+ *
+ * <p>By default, floating-point fields are compared using exact equality, which is <a
+ * href="https://truth.dev/floating_point">probably not what you want</a> if the values are the
+ * results of some arithmetic. To check for approximate equality, use {@link
+ * #usingDoubleToleranceForValues}, {@link #usingFloatToleranceForValues}, and {@linkplain
+ * #usingDoubleToleranceForFieldsForValues(double, int, int...) their per-field equivalents}.
+ *
+ * <p>Equality tests, and other methods, may yield slightly different behavior for versions 2 and 3
+ * of Protocol Buffers. If testing protos of multiple versions, make sure you understand the
+ * behaviors of default and unknown fields so you don't under or over test.
+ *
+ * @param <M> the type of the message values in the map
+ */
+public class MapWithProtoValuesSubject<M extends Message> extends MapSubject {
+
+ /*
+ * Storing a FailureMetadata instance in a Subject subclass is generally a bad practice. For an
+ * explanation of why it works out OK here, see LiteProtoSubject.
+ */
+ private final FailureMetadata metadata;
+ private final Map<?, M> actual;
+ private final FluentEqualityConfig config;
+
+ protected MapWithProtoValuesSubject(FailureMetadata failureMetadata, @Nullable Map<?, M> map) {
+ this(failureMetadata, FluentEqualityConfig.defaultInstance(), map);
+ }
+
+ MapWithProtoValuesSubject(
+ FailureMetadata failureMetadata, FluentEqualityConfig config, @Nullable Map<?, M> map) {
+ super(failureMetadata, map);
+ this.metadata = failureMetadata;
+ this.actual = map;
+ this.config = config;
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // MapWithProtoValuesFluentAssertion Configuration
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+
+ MapWithProtoValuesFluentAssertion<M> usingConfig(FluentEqualityConfig newConfig) {
+ return new MapWithProtoValuesFluentAssertionImpl<>(
+ new MapWithProtoValuesSubject<>(metadata, newConfig, actual));
+ }
+
+ /**
+ * Specifies that the 'has' bit of individual fields should be ignored when comparing for
+ * equality.
+ *
+ * <p>For version 2 Protocol Buffers, this setting determines whether two protos with the same
+ * value for a field compare equal if one explicitly sets the value, and the other merely
+ * implicitly uses the schema-defined default. This setting also determines whether unknown fields
+ * should be considered in the comparison. By {@code ignoringFieldAbsence()}, unknown fields are
+ * ignored, and value-equal fields as specified above are considered equal.
+ *
+ * <p>For version 3 Protocol Buffers, this setting does not affect primitive fields, because their
+ * default value is indistinguishable from unset.
+ */
+ public MapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceForValues() {
+ return usingConfig(config.ignoringFieldAbsence());
+ }
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified top-level field numbers should be
+ * ignored when comparing for equality. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if they are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsenceForValues()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsenceForValues() for details
+ */
+ public MapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceOfFieldsForValues(
+ int firstFieldNumber, int... rest) {
+ return usingConfig(config.ignoringFieldAbsenceOfFields(asList(firstFieldNumber, rest)));
+ }
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified top-level field numbers should be
+ * ignored when comparing for equality. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if they are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsenceForValues()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsenceForValues() for details
+ */
+ public MapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceOfFieldsForValues(
+ Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.ignoringFieldAbsenceOfFields(fieldNumbers));
+ }
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified field descriptors should be ignored
+ * when comparing for equality. Sub-fields must be specified explicitly if they are to be ignored
+ * as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsenceForValues()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsenceForValues() for details
+ */
+ public MapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceOfFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return usingConfig(
+ config.ignoringFieldAbsenceOfFieldDescriptors(asList(firstFieldDescriptor, rest)));
+ }
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified field descriptors should be ignored
+ * when comparing for equality. Sub-fields must be specified explicitly if they are to be ignored
+ * as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsenceForValues()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsenceForValues() for details
+ */
+ public MapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceOfFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(config.ignoringFieldAbsenceOfFieldDescriptors(fieldDescriptors));
+ }
+
+ /**
+ * Specifies that the ordering of repeated fields, at all levels, should be ignored when comparing
+ * for equality.
+ *
+ * <p>This setting applies to all repeated fields recursively, but it does not ignore structure.
+ * For example, with {@link #ignoringRepeatedFieldOrderForValues()}, a repeated {@code int32}
+ * field {@code bar}, set inside a repeated message field {@code foo}, the following protos will
+ * all compare equal:
+ *
+ * <pre>{@code
+ * message1: {
+ * foo: {
+ * bar: 1
+ * bar: 2
+ * }
+ * foo: {
+ * bar: 3
+ * bar: 4
+ * }
+ * }
+ *
+ * message2: {
+ * foo: {
+ * bar: 2
+ * bar: 1
+ * }
+ * foo: {
+ * bar: 4
+ * bar: 3
+ * }
+ * }
+ *
+ * message3: {
+ * foo: {
+ * bar: 4
+ * bar: 3
+ * }
+ * foo: {
+ * bar: 2
+ * bar: 1
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>However, the following message will compare equal to none of these:
+ *
+ * <pre>{@code
+ * message4: {
+ * foo: {
+ * bar: 1
+ * bar: 3
+ * }
+ * foo: {
+ * bar: 2
+ * bar: 4
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>This setting does not apply to map fields, for which field order is always ignored. The
+ * serialization order of map fields is undefined, and it may change from runtime to runtime.
+ */
+ public MapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderForValues() {
+ return usingConfig(config.ignoringRepeatedFieldOrder());
+ }
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified top-level field
+ * numbers should be ignored when comparing for equality. Sub-fields must be specified explicitly
+ * (via {@link FieldDescriptor}) if their orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrderForValues()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrderForValues() for details.
+ */
+ public MapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldsForValues(
+ int firstFieldNumber, int... rest) {
+ return usingConfig(config.ignoringRepeatedFieldOrderOfFields(asList(firstFieldNumber, rest)));
+ }
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified top-level field
+ * numbers should be ignored when comparing for equality. Sub-fields must be specified explicitly
+ * (via {@link FieldDescriptor}) if their orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrderForValues()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrderForValues() for details.
+ */
+ public MapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldsForValues(
+ Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.ignoringRepeatedFieldOrderOfFields(fieldNumbers));
+ }
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified field descriptors
+ * should be ignored when comparing for equality. Sub-fields must be specified explicitly if their
+ * orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrderForValues()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrderForValues() for details.
+ */
+ public MapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return usingConfig(
+ config.ignoringRepeatedFieldOrderOfFieldDescriptors(asList(firstFieldDescriptor, rest)));
+ }
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified field descriptors
+ * should be ignored when comparing for equality. Sub-fields must be specified explicitly if their
+ * orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrderForValues()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrderForValues() for details.
+ */
+ public MapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(config.ignoringRepeatedFieldOrderOfFieldDescriptors(fieldDescriptors));
+ }
+
+ /**
+ * Specifies that, for all repeated and map fields, any elements in the 'actual' proto which are
+ * not found in the 'expected' proto are ignored, with the exception of fields in the expected
+ * proto which are empty. To ignore empty repeated fields as well, use {@link
+ * #comparingExpectedFieldsOnlyForValues}.
+ *
+ * <p>This rule is applied independently from {@link #ignoringRepeatedFieldOrderForValues}. If
+ * ignoring repeated field order AND extra repeated field elements, all that is tested is that the
+ * expected elements comprise a subset of the actual elements. If not ignoring repeated field
+ * order, but still ignoring extra repeated field elements, the actual elements must contain a
+ * subsequence that matches the expected elements for the test to pass. (The subsequence rule does
+ * not apply to Map fields, which are always compared by key.)
+ */
+ public MapWithProtoValuesFluentAssertion<M> ignoringExtraRepeatedFieldElementsForValues() {
+ return usingConfig(config.ignoringExtraRepeatedFieldElements());
+ }
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified top-level field
+ * numbers should be ignored. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if their extra elements are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElementsForValues()} instead to ignore these for all
+ * fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElementsForValues() for details.
+ */
+ public MapWithProtoValuesFluentAssertion<M> ignoringExtraRepeatedFieldElementsOfFieldsForValues(
+ int firstFieldNumber, int... rest) {
+ return usingConfig(
+ config.ignoringExtraRepeatedFieldElementsOfFields(asList(firstFieldNumber, rest)));
+ }
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified top-level field
+ * numbers should be ignored. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if their extra elements are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElementsForValues()} instead to ignore these for all
+ * fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElementsForValues() for details.
+ */
+ public MapWithProtoValuesFluentAssertion<M> ignoringExtraRepeatedFieldElementsOfFieldsForValues(
+ Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.ignoringExtraRepeatedFieldElementsOfFields(fieldNumbers));
+ }
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified field descriptors
+ * should be ignored. Sub-fields must be specified explicitly if their extra elements are to be
+ * ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElementsForValues()} instead to ignore these for all
+ * fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElementsForValues() for details.
+ */
+ public MapWithProtoValuesFluentAssertion<M>
+ ignoringExtraRepeatedFieldElementsOfFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return usingConfig(
+ config.ignoringExtraRepeatedFieldElementsOfFieldDescriptors(
+ asList(firstFieldDescriptor, rest)));
+ }
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified field descriptors
+ * should be ignored. Sub-fields must be specified explicitly if their extra elements are to be
+ * ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElementsForValues()} instead to ignore these for all
+ * fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElementsForValues() for details.
+ */
+ public MapWithProtoValuesFluentAssertion<M>
+ ignoringExtraRepeatedFieldElementsOfFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(
+ config.ignoringExtraRepeatedFieldElementsOfFieldDescriptors(fieldDescriptors));
+ }
+
+ /**
+ * Compares double fields as equal if they are both finite and their absolute difference is less
+ * than or equal to {@code tolerance}.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public MapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForValues(double tolerance) {
+ return usingConfig(config.usingDoubleTolerance(tolerance));
+ }
+
+ /**
+ * Compares double fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public MapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForFieldsForValues(
+ double tolerance, int firstFieldNumber, int... rest) {
+ return usingConfig(
+ config.usingDoubleToleranceForFields(tolerance, asList(firstFieldNumber, rest)));
+ }
+
+ /**
+ * Compares double fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public MapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForFieldsForValues(
+ double tolerance, Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.usingDoubleToleranceForFields(tolerance, fieldNumbers));
+ }
+
+ /**
+ * Compares double fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public MapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForFieldDescriptorsForValues(
+ double tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return usingConfig(
+ config.usingDoubleToleranceForFieldDescriptors(
+ tolerance, asList(firstFieldDescriptor, rest)));
+ }
+
+ /**
+ * Compares double fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public MapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForFieldDescriptorsForValues(
+ double tolerance, Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(config.usingDoubleToleranceForFieldDescriptors(tolerance, fieldDescriptors));
+ }
+
+ /**
+ * Compares float fields as equal if they are both finite and their absolute difference is less
+ * than or equal to {@code tolerance}.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public MapWithProtoValuesFluentAssertion<M> usingFloatToleranceForValues(float tolerance) {
+ return usingConfig(config.usingFloatTolerance(tolerance));
+ }
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public MapWithProtoValuesFluentAssertion<M> usingFloatToleranceForFieldsForValues(
+ float tolerance, int firstFieldNumber, int... rest) {
+ return usingConfig(
+ config.usingFloatToleranceForFields(tolerance, asList(firstFieldNumber, rest)));
+ }
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public MapWithProtoValuesFluentAssertion<M> usingFloatToleranceForFieldsForValues(
+ float tolerance, Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.usingFloatToleranceForFields(tolerance, fieldNumbers));
+ }
+
+ /**
+ * Compares float fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public MapWithProtoValuesFluentAssertion<M> usingFloatToleranceForFieldDescriptorsForValues(
+ float tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return usingConfig(
+ config.usingFloatToleranceForFieldDescriptors(
+ tolerance, asList(firstFieldDescriptor, rest)));
+ }
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public MapWithProtoValuesFluentAssertion<M> usingFloatToleranceForFieldDescriptorsForValues(
+ float tolerance, Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(config.usingFloatToleranceForFieldDescriptors(tolerance, fieldDescriptors));
+ }
+
+ /**
+ * Limits the comparison of Protocol buffers to the fields set in the expected proto(s). When
+ * multiple protos are specified, the comparison is limited to the union of set fields in all the
+ * expected protos.
+ *
+ * <p>The "expected proto(s)" are those passed to the method at the end of the call chain, such as
+ * {@link #containsEntry} or {@link #containsExactlyEntriesIn}.
+ *
+ * <p>Fields not set in the expected proto(s) are ignored. In particular, proto3 fields which have
+ * their default values are ignored, as these are indistinguishable from unset fields. If you want
+ * to assert that a proto3 message has certain fields with default values, you cannot use this
+ * method.
+ */
+ public MapWithProtoValuesFluentAssertion<M> comparingExpectedFieldsOnlyForValues() {
+ return usingConfig(config.comparingExpectedFieldsOnly());
+ }
+
+ /**
+ * Limits the comparison of Protocol buffers to the defined {@link FieldScope}.
+ *
+ * <p>This method is additive and has well-defined ordering semantics. If the invoking {@link
+ * ProtoFluentAssertion} is already scoped to a {@link FieldScope} {@code X}, and this method is
+ * invoked with {@link FieldScope} {@code Y}, the resultant {@link ProtoFluentAssertion} is
+ * constrained to the intersection of {@link FieldScope}s {@code X} and {@code Y}.
+ *
+ * <p>By default, {@link MapWithProtoValuesFluentAssertion} is constrained to {@link
+ * FieldScopes#all()}, that is, no fields are excluded from comparison.
+ */
+ public MapWithProtoValuesFluentAssertion<M> withPartialScopeForValues(FieldScope fieldScope) {
+ return usingConfig(config.withPartialScope(checkNotNull(fieldScope, "fieldScope")));
+ }
+
+ /**
+ * Excludes the top-level message fields with the given tag numbers from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * numbers are ignored, and all sub-messages of type {@code M} will also have these field numbers
+ * ignored.
+ *
+ * <p>If an invalid field number is supplied, the terminal comparison operation will throw a
+ * runtime exception.
+ */
+ public MapWithProtoValuesFluentAssertion<M> ignoringFieldsForValues(
+ int firstFieldNumber, int... rest) {
+ return ignoringFieldsForValues(asList(firstFieldNumber, rest));
+ }
+
+ /**
+ * Excludes the top-level message fields with the given tag numbers from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * numbers are ignored, and all sub-messages of type {@code M} will also have these field numbers
+ * ignored.
+ *
+ * <p>If an invalid field number is supplied, the terminal comparison operation will throw a
+ * runtime exception.
+ */
+ public MapWithProtoValuesFluentAssertion<M> ignoringFieldsForValues(
+ Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.ignoringFields(fieldNumbers));
+ }
+
+ /**
+ * Excludes all message fields matching the given {@link FieldDescriptor}s from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * descriptors are ignored, no matter where they occur in the tree.
+ *
+ * <p>If a field descriptor which does not, or cannot occur in the proto structure is supplied, it
+ * is silently ignored.
+ */
+ public MapWithProtoValuesFluentAssertion<M> ignoringFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return ignoringFieldDescriptorsForValues(asList(firstFieldDescriptor, rest));
+ }
+
+ /**
+ * Excludes all message fields matching the given {@link FieldDescriptor}s from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * descriptors are ignored, no matter where they occur in the tree.
+ *
+ * <p>If a field descriptor which does not, or cannot occur in the proto structure is supplied, it
+ * is silently ignored.
+ */
+ public MapWithProtoValuesFluentAssertion<M> ignoringFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(config.ignoringFieldDescriptors(fieldDescriptors));
+ }
+
+ /**
+ * Excludes all specific field paths under the argument {@link FieldScope} from the comparison.
+ *
+ * <p>This method is additive and has well-defined ordering semantics. If the invoking {@link
+ * ProtoFluentAssertion} is already scoped to a {@link FieldScope} {@code X}, and this method is
+ * invoked with {@link FieldScope} {@code Y}, the resultant {@link ProtoFluentAssertion} is
+ * constrained to the subtraction of {@code X - Y}.
+ *
+ * <p>By default, {@link ProtoFluentAssertion} is constrained to {@link FieldScopes#all()}, that
+ * is, no fields are excluded from comparison.
+ */
+ public MapWithProtoValuesFluentAssertion<M> ignoringFieldScopeForValues(FieldScope fieldScope) {
+ return usingConfig(config.ignoringFieldScope(checkNotNull(fieldScope, "fieldScope")));
+ }
+
+ /**
+ * If set, in the event of a comparison failure, the error message printed will list only those
+ * specific fields that did not match between the actual and expected values. Useful for very
+ * large protocol buffers.
+ *
+ * <p>This a purely cosmetic setting, and it has no effect on the behavior of the test.
+ */
+ public MapWithProtoValuesFluentAssertion<M> reportingMismatchesOnlyForValues() {
+ return usingConfig(config.reportingMismatchesOnly());
+ }
+
+ /**
+ * Specifies the {@link TypeRegistry} and {@link ExtensionRegistry} to use for {@link
+ * com.google.protobuf.Any Any} messages.
+ *
+ * <p>To compare the value of an {@code Any} message, ProtoTruth looks in the given type registry
+ * for a descriptor for the message's type URL:
+ *
+ * <ul>
+ * <li>If ProtoTruth finds a descriptor, it unpacks the value and compares it against the
+ * expected value, respecting any configuration methods used for the assertion.
+ * <li>If ProtoTruth does not find a descriptor (or if the value can't be deserialized with the
+ * descriptor), it compares the raw, serialized bytes of the expected and actual values.
+ * </ul>
+ *
+ * <p>When ProtoTruth unpacks a value, it is parsing a serialized proto. That proto may contain
+ * extensions. To look up those extensions, ProtoTruth uses the provided {@link
+ * ExtensionRegistry}.
+ *
+ * @since 1.1
+ */
+ public MapWithProtoValuesFluentAssertion<M> unpackingAnyUsingForValues(
+ TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry) {
+ return usingConfig(config.unpackingAnyUsing(typeRegistry, extensionRegistry));
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // UsingCorrespondence Methods
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+
+ private MapSubject.UsingCorrespondence<M, M> usingCorrespondence(
+ Iterable<? extends M> expectedValues) {
+ return comparingValuesUsing(
+ config
+ .withExpectedMessages(expectedValues)
+ .<M>toCorrespondence(FieldScopeUtil.getSingleDescriptor(actual.values())));
+ }
+
+ // The UsingCorrespondence methods have conflicting erasure with default MapSubject methods,
+ // so we can't implement them both on the same class, but we want to define both so
+ // MapWithProtoValuesSubjects are interchangeable with MapSubjects when no configuration is
+ // specified. So, we implement a dumb, private delegator to return instead.
+ private static final class MapWithProtoValuesFluentAssertionImpl<M extends Message>
+ implements MapWithProtoValuesFluentAssertion<M> {
+ private final MapWithProtoValuesSubject<M> subject;
+
+ MapWithProtoValuesFluentAssertionImpl(MapWithProtoValuesSubject<M> subject) {
+ this.subject = subject;
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceForValues() {
+ return subject.ignoringFieldAbsenceForValues();
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceOfFieldsForValues(
+ int firstFieldNumber, int... rest) {
+ return subject.ignoringFieldAbsenceOfFieldsForValues(firstFieldNumber, rest);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceOfFieldsForValues(
+ Iterable<Integer> fieldNumbers) {
+ return subject.ignoringFieldAbsenceOfFieldsForValues(fieldNumbers);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceOfFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return subject.ignoringFieldAbsenceOfFieldDescriptorsForValues(firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceOfFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return subject.ignoringFieldAbsenceOfFieldDescriptorsForValues(fieldDescriptors);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderForValues() {
+ return subject.ignoringRepeatedFieldOrderForValues();
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldsForValues(
+ int firstFieldNumber, int... rest) {
+ return subject.ignoringRepeatedFieldOrderOfFieldsForValues(firstFieldNumber, rest);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldsForValues(
+ Iterable<Integer> fieldNumbers) {
+ return subject.ignoringRepeatedFieldOrderOfFieldsForValues(fieldNumbers);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M>
+ ignoringRepeatedFieldOrderOfFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return subject.ignoringRepeatedFieldOrderOfFieldDescriptorsForValues(
+ firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M>
+ ignoringRepeatedFieldOrderOfFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return subject.ignoringRepeatedFieldOrderOfFieldDescriptorsForValues(fieldDescriptors);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> ignoringExtraRepeatedFieldElementsForValues() {
+ return subject.ignoringExtraRepeatedFieldElementsForValues();
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> ignoringExtraRepeatedFieldElementsOfFieldsForValues(
+ int firstFieldNumber, int... rest) {
+ return subject.ignoringExtraRepeatedFieldElementsOfFieldsForValues(firstFieldNumber, rest);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> ignoringExtraRepeatedFieldElementsOfFieldsForValues(
+ Iterable<Integer> fieldNumbers) {
+ return subject.ignoringExtraRepeatedFieldElementsOfFieldsForValues(fieldNumbers);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M>
+ ignoringExtraRepeatedFieldElementsOfFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return subject.ignoringExtraRepeatedFieldElementsOfFieldDescriptorsForValues(
+ firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M>
+ ignoringExtraRepeatedFieldElementsOfFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return subject.ignoringExtraRepeatedFieldElementsOfFieldDescriptorsForValues(
+ fieldDescriptors);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForValues(double tolerance) {
+ return subject.usingDoubleToleranceForValues(tolerance);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForFieldsForValues(
+ double tolerance, int firstFieldNumber, int... rest) {
+ return subject.usingDoubleToleranceForFieldsForValues(tolerance, firstFieldNumber, rest);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForFieldsForValues(
+ double tolerance, Iterable<Integer> fieldNumbers) {
+ return subject.usingDoubleToleranceForFieldsForValues(tolerance, fieldNumbers);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForFieldDescriptorsForValues(
+ double tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return subject.usingDoubleToleranceForFieldDescriptorsForValues(
+ tolerance, firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForFieldDescriptorsForValues(
+ double tolerance, Iterable<FieldDescriptor> fieldDescriptors) {
+ return subject.usingDoubleToleranceForFieldDescriptorsForValues(tolerance, fieldDescriptors);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> usingFloatToleranceForValues(float tolerance) {
+ return subject.usingFloatToleranceForValues(tolerance);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> usingFloatToleranceForFieldsForValues(
+ float tolerance, int firstFieldNumber, int... rest) {
+ return subject.usingFloatToleranceForFieldsForValues(tolerance, firstFieldNumber, rest);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> usingFloatToleranceForFieldsForValues(
+ float tolerance, Iterable<Integer> fieldNumbers) {
+ return subject.usingFloatToleranceForFieldsForValues(tolerance, fieldNumbers);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> usingFloatToleranceForFieldDescriptorsForValues(
+ float tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return subject.usingFloatToleranceForFieldDescriptorsForValues(
+ tolerance, firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> usingFloatToleranceForFieldDescriptorsForValues(
+ float tolerance, Iterable<FieldDescriptor> fieldDescriptors) {
+ return subject.usingFloatToleranceForFieldDescriptorsForValues(tolerance, fieldDescriptors);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> comparingExpectedFieldsOnlyForValues() {
+ return subject.comparingExpectedFieldsOnlyForValues();
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> withPartialScopeForValues(FieldScope fieldScope) {
+ return subject.withPartialScopeForValues(fieldScope);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> ignoringFieldsForValues(
+ int firstFieldNumber, int... rest) {
+ return subject.ignoringFieldsForValues(firstFieldNumber, rest);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> ignoringFieldsForValues(
+ Iterable<Integer> fieldNumbers) {
+ return subject.ignoringFieldsForValues(fieldNumbers);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> ignoringFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return subject.ignoringFieldDescriptorsForValues(firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> ignoringFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return subject.ignoringFieldDescriptorsForValues(fieldDescriptors);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> ignoringFieldScopeForValues(FieldScope fieldScope) {
+ return subject.ignoringFieldScopeForValues(fieldScope);
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> reportingMismatchesOnlyForValues() {
+ return subject.reportingMismatchesOnlyForValues();
+ }
+
+ @Override
+ public MapWithProtoValuesFluentAssertion<M> unpackingAnyUsingForValues(
+ TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry) {
+ return subject.unpackingAnyUsingForValues(typeRegistry, extensionRegistry);
+ }
+
+ @Override
+ public void containsEntry(@Nullable Object expectedKey, @Nullable M expectedValue) {
+ subject
+ .usingCorrespondence(Arrays.asList(expectedValue))
+ .containsEntry(expectedKey, expectedValue);
+ }
+
+ @Override
+ public void doesNotContainEntry(@Nullable Object excludedKey, @Nullable M excludedValue) {
+ subject
+ .usingCorrespondence(Arrays.asList(excludedValue))
+ .doesNotContainEntry(excludedKey, excludedValue);
+ }
+
+ @Override
+ @CanIgnoreReturnValue
+ @SuppressWarnings("unchecked") // ClassCastException is fine
+ public Ordered containsExactly(
+ @Nullable Object k0, @Nullable M v0, /*@Nullable*/ Object... rest) {
+ List<M> expectedValues = new ArrayList<>();
+ expectedValues.add(v0);
+ for (int i = 1; i < rest.length; i += 2) {
+ expectedValues.add((M) rest[i]);
+ }
+ return subject.usingCorrespondence(expectedValues).containsExactly(k0, v0, rest);
+ }
+
+ @Override
+ @CanIgnoreReturnValue
+ public Ordered containsExactlyEntriesIn(Map<?, ? extends M> expectedMap) {
+ return subject
+ .usingCorrespondence(expectedMap.values())
+ .containsExactlyEntriesIn(expectedMap);
+ }
+
+ @SuppressWarnings("DoNotCall")
+ @Override
+ @Deprecated
+ public boolean equals(Object o) {
+ return subject.equals(o);
+ }
+
+ @SuppressWarnings("DoNotCall")
+ @Override
+ @Deprecated
+ public int hashCode() {
+ return subject.hashCode();
+ }
+ }
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/MultimapWithProtoValuesFluentAssertion.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/MultimapWithProtoValuesFluentAssertion.java
new file mode 100644
index 00000000..224e3302
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/MultimapWithProtoValuesFluentAssertion.java
@@ -0,0 +1,556 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import com.google.common.collect.Multimap;
+import com.google.common.truth.Ordered;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.ExtensionRegistry;
+import com.google.protobuf.Message;
+import com.google.protobuf.TypeRegistry;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Fluent API to perform detailed, customizable comparison of {@link Multimap}s containing protocol
+ * buffers as values. The same comparison rules are applied to all pairs of protocol buffers which
+ * get compared.
+ *
+ * <p>The <b>keys</b> of these maps are treated as ordinary objects, and keys which happen to be
+ * protocol buffers are not given special treatment. They are compared with {@link Object#equals}
+ * and {@link Object#hashCode()} as documented by the {@link Multimap} interface.
+ *
+ * <p>Methods may be chained in any order, but the chain should terminate with a method that doesn't
+ * return a {@code MultimapWithProtoValuesFluentAssertion}, such as {@link
+ * #containsExactlyEntriesIn} or {@link #containsEntry}.
+ *
+ * <p>The state of a {@code MultimapWithProtoValuesFluentAssertion} object after each method is
+ * called is left undefined. Users should not retain references to {@code
+ * MultimapWithProtoValuesFluentAssertion} instances.
+ */
+public interface MultimapWithProtoValuesFluentAssertion<M extends Message> {
+
+ /**
+ * Specifies that the 'has' bit of individual fields should be ignored when comparing for
+ * equality.
+ *
+ * <p>For version 2 Protocol Buffers, this setting determines whether two protos with the same
+ * value for a field compare equal if one explicitly sets the value, and the other merely
+ * implicitly uses the schema-defined default. This setting also determines whether unknown fields
+ * should be considered in the comparison. By {@code ignoringFieldAbsence()}, unknown fields are
+ * ignored, and value-equal fields as specified above are considered equal.
+ *
+ * <p>For version 3 Protocol Buffers, this setting does not affect primitive fields, because their
+ * default value is indistinguishable from unset.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceForValues();
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified top-level field numbers should be
+ * ignored when comparing for equality. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if they are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsenceForValues()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsenceForValues() for details
+ */
+ MultimapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceOfFieldsForValues(
+ int firstFieldNumber, int... rest);
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified top-level field numbers should be
+ * ignored when comparing for equality. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if they are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsenceForValues()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsenceForValues() for details
+ */
+ MultimapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceOfFieldsForValues(
+ Iterable<Integer> fieldNumbers);
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified field descriptors should be ignored
+ * when comparing for equality. Sub-fields must be specified explicitly if they are to be ignored
+ * as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsenceForValues()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsenceForValues() for details
+ */
+ MultimapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceOfFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified field descriptors should be ignored
+ * when comparing for equality. Sub-fields must be specified explicitly if they are to be ignored
+ * as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsenceForValues()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsenceForValues() for details
+ */
+ MultimapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceOfFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Specifies that the ordering of repeated fields, at all levels, should be ignored when comparing
+ * for equality.
+ *
+ * <p>This setting applies to all repeated fields recursively, but it does not ignore structure.
+ * For example, with {@link #ignoringRepeatedFieldOrderForValues()}, a repeated {@code int32}
+ * field {@code bar}, set inside a repeated message field {@code foo}, the following protos will
+ * all compare equal:
+ *
+ * <pre>{@code
+ * message1: {
+ * foo: {
+ * bar: 1
+ * bar: 2
+ * }
+ * foo: {
+ * bar: 3
+ * bar: 4
+ * }
+ * }
+ *
+ * message2: {
+ * foo: {
+ * bar: 2
+ * bar: 1
+ * }
+ * foo: {
+ * bar: 4
+ * bar: 3
+ * }
+ * }
+ *
+ * message3: {
+ * foo: {
+ * bar: 4
+ * bar: 3
+ * }
+ * foo: {
+ * bar: 2
+ * bar: 1
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>However, the following message will compare equal to none of these:
+ *
+ * <pre>{@code
+ * message4: {
+ * foo: {
+ * bar: 1
+ * bar: 3
+ * }
+ * foo: {
+ * bar: 2
+ * bar: 4
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>This setting does not apply to map fields, for which field order is always ignored. The
+ * serialization order of map fields is undefined, and it may change from runtime to runtime.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderForValues();
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified top-level field
+ * numbers should be ignored when comparing for equality. Sub-fields must be specified explicitly
+ * (via {@link FieldDescriptor}) if their orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrderForValues()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrderForValues() for details.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldsForValues(
+ int firstFieldNumber, int... rest);
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified top-level field
+ * numbers should be ignored when comparing for equality. Sub-fields must be specified explicitly
+ * (via {@link FieldDescriptor}) if their orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrderForValues()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrderForValues() for details.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldsForValues(
+ Iterable<Integer> fieldNumbers);
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified field descriptors
+ * should be ignored when comparing for equality. Sub-fields must be specified explicitly if their
+ * orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrderForValues()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrderForValues() for details.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified field descriptors
+ * should be ignored when comparing for equality. Sub-fields must be specified explicitly if their
+ * orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrderForValues()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrderForValues() for details.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Specifies that, for all repeated and map fields, any elements in the 'actual' proto which are
+ * not found in the 'expected' proto are ignored, with the exception of fields in the expected
+ * proto which are empty. To ignore empty repeated fields as well, use {@link
+ * #comparingExpectedFieldsOnlyForValues}.
+ *
+ * <p>This rule is applied independently from {@link #ignoringRepeatedFieldOrderForValues}. If
+ * ignoring repeated field order AND extra repeated field elements, all that is tested is that the
+ * expected elements comprise a subset of the actual elements. If not ignoring repeated field
+ * order, but still ignoring extra repeated field elements, the actual elements must contain a
+ * subsequence that matches the expected elements for the test to pass. (The subsequence rule does
+ * not apply to Map fields, which are always compared by key.)
+ */
+ MultimapWithProtoValuesFluentAssertion<M> ignoringExtraRepeatedFieldElementsForValues();
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified top-level field
+ * numbers should be ignored. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if their extra elements are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElementsForValues()} instead to ignore these for all
+ * fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElementsForValues() for details.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> ignoringExtraRepeatedFieldElementsOfFieldsForValues(
+ int firstFieldNumber, int... rest);
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified top-level field
+ * numbers should be ignored. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if their extra elements are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElementsForValues()} instead to ignore these for all
+ * fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElementsForValues() for details.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> ignoringExtraRepeatedFieldElementsOfFieldsForValues(
+ Iterable<Integer> fieldNumbers);
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified field descriptors
+ * should be ignored. Sub-fields must be specified explicitly if their extra elements are to be
+ * ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElementsForValues()} instead to ignore these for all
+ * fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElementsForValues() for details.
+ */
+ MultimapWithProtoValuesFluentAssertion<M>
+ ignoringExtraRepeatedFieldElementsOfFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified field descriptors
+ * should be ignored. Sub-fields must be specified explicitly if their extra elements are to be
+ * ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElementsForValues()} instead to ignore these for all
+ * fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElementsForValues() for details.
+ */
+ MultimapWithProtoValuesFluentAssertion<M>
+ ignoringExtraRepeatedFieldElementsOfFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Compares double fields as equal if they are both finite and their absolute difference is less
+ * than or equal to {@code tolerance}.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForValues(double tolerance);
+
+ /**
+ * Compares double fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForFieldsForValues(
+ double tolerance, int firstFieldNumber, int... rest);
+
+ /**
+ * Compares double fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForFieldsForValues(
+ double tolerance, Iterable<Integer> fieldNumbers);
+
+ /**
+ * Compares double fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForFieldDescriptorsForValues(
+ double tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Compares double fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForFieldDescriptorsForValues(
+ double tolerance, Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Compares float fields as equal if they are both finite and their absolute difference is less
+ * than or equal to {@code tolerance}.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> usingFloatToleranceForValues(float tolerance);
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> usingFloatToleranceForFieldsForValues(
+ float tolerance, int firstFieldNumber, int... rest);
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> usingFloatToleranceForFieldsForValues(
+ float tolerance, Iterable<Integer> fieldNumbers);
+
+ /**
+ * Compares float fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> usingFloatToleranceForFieldDescriptorsForValues(
+ float tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> usingFloatToleranceForFieldDescriptorsForValues(
+ float tolerance, Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Limits the comparison of Protocol buffers to the fields set in the expected proto(s). When
+ * multiple protos are specified, the comparison is limited to the union of set fields in all the
+ * expected protos.
+ *
+ * <p>The "expected proto(s)" are those passed to the method at the end of the call chain, such as
+ * {@link #containsEntry} or {@link #containsExactlyEntriesIn}.
+ *
+ * <p>Fields not set in the expected proto(s) are ignored. In particular, proto3 fields which have
+ * their default values are ignored, as these are indistinguishable from unset fields. If you want
+ * to assert that a proto3 message has certain fields with default values, you cannot use this
+ * method.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> comparingExpectedFieldsOnlyForValues();
+
+ /**
+ * Limits the comparison of Protocol buffers to the defined {@link FieldScope}.
+ *
+ * <p>This method is additive and has well-defined ordering semantics. If the invoking {@link
+ * MultimapWithProtoValuesFluentAssertion} is already scoped to a {@link FieldScope} {@code X},
+ * and this method is invoked with {@link FieldScope} {@code Y}, the resultant {@link
+ * MultimapWithProtoValuesFluentAssertion} is constrained to the intersection of {@link
+ * FieldScope}s {@code X} and {@code Y}.
+ *
+ * <p>By default, {@link MultimapWithProtoValuesFluentAssertion} is constrained to {@link
+ * FieldScopes#all()}, that is, no fields are excluded from comparison.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> withPartialScopeForValues(FieldScope fieldScope);
+
+ /**
+ * Excludes the top-level message fields with the given tag numbers from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * numbers are ignored, and all sub-messages of type {@code M} will also have these field numbers
+ * ignored.
+ *
+ * <p>If an invalid field number is supplied, the terminal comparison operation will throw a
+ * runtime exception.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> ignoringFieldsForValues(
+ int firstFieldNumber, int... rest);
+
+ /**
+ * Excludes the top-level message fields with the given tag numbers from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * numbers are ignored, and all sub-messages of type {@code M} will also have these field numbers
+ * ignored.
+ *
+ * <p>If an invalid field number is supplied, the terminal comparison operation will throw a
+ * runtime exception.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> ignoringFieldsForValues(Iterable<Integer> fieldNumbers);
+
+ /**
+ * Excludes all message fields matching the given {@link FieldDescriptor}s from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * descriptors are ignored, no matter where they occur in the tree.
+ *
+ * <p>If a field descriptor which does not, or cannot occur in the proto structure is supplied, it
+ * is silently ignored.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> ignoringFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Excludes all message fields matching the given {@link FieldDescriptor}s from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * descriptors are ignored, no matter where they occur in the tree.
+ *
+ * <p>If a field descriptor which does not, or cannot occur in the proto structure is supplied, it
+ * is silently ignored.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> ignoringFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Excludes all specific field paths under the argument {@link FieldScope} from the comparison.
+ *
+ * <p>This method is additive and has well-defined ordering semantics. If the invoking {@link
+ * MultimapWithProtoValuesFluentAssertion} is already scoped to a {@link FieldScope} {@code X},
+ * and this method is invoked with {@link FieldScope} {@code Y}, the resultant {@link
+ * MultimapWithProtoValuesFluentAssertion} is constrained to the subtraction of {@code X - Y}.
+ *
+ * <p>By default, {@link MultimapWithProtoValuesFluentAssertion} is constrained to {@link
+ * FieldScopes#all()}, that is, no fields are excluded from comparison.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> ignoringFieldScopeForValues(FieldScope fieldScope);
+
+ /**
+ * If set, in the event of a comparison failure, the error message printed will list only those
+ * specific fields that did not match between the actual and expected values. Useful for very
+ * large protocol buffers.
+ *
+ * <p>This a purely cosmetic setting, and it has no effect on the behavior of the test.
+ */
+ MultimapWithProtoValuesFluentAssertion<M> reportingMismatchesOnlyForValues();
+
+ /**
+ * Specifies the {@link TypeRegistry} and {@link ExtensionRegistry} to use for {@link
+ * com.google.protobuf.Any Any} messages.
+ *
+ * <p>To compare the value of an {@code Any} message, ProtoTruth looks in the given type registry
+ * for a descriptor for the message's type URL:
+ *
+ * <ul>
+ * <li>If ProtoTruth finds a descriptor, it unpacks the value and compares it against the
+ * expected value, respecting any configuration methods used for the assertion.
+ * <li>If ProtoTruth does not find a descriptor (or if the value can't be deserialized with the
+ * descriptor), it compares the raw, serialized bytes of the expected and actual values.
+ * </ul>
+ *
+ * <p>When ProtoTruth unpacks a value, it is parsing a serialized proto. That proto may contain
+ * extensions. To look up those extensions, ProtoTruth uses the provided {@link
+ * ExtensionRegistry}.
+ *
+ * @since 1.1
+ */
+ MultimapWithProtoValuesFluentAssertion<M> unpackingAnyUsingForValues(
+ TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry);
+
+ /**
+ * Fails if the multimap does not contain an entry with the given key and a value that corresponds
+ * to the given value.
+ */
+ void containsEntry(@Nullable Object expectedKey, @Nullable M expectedValue);
+
+ /**
+ * Fails if the multimap contains an entry with the given key and a value that corresponds to the
+ * given value.
+ */
+ void doesNotContainEntry(@Nullable Object excludedKey, @Nullable M excludedValue);
+
+ /**
+ * Fails if the map does not contain exactly the keys in the given multimap, mapping to values
+ * that correspond to the values of the given multimap.
+ *
+ * <p>A subsequent call to {@link Ordered#inOrder} may be made if the caller wishes to verify that
+ * the two Multimaps iterate fully in the same order. That is, their key sets iterate in the same
+ * order, and the corresponding value collections for each key iterate in the same order.
+ */
+ @CanIgnoreReturnValue
+ Ordered containsExactlyEntriesIn(Multimap<?, ? extends M> expectedMultimap);
+
+ /** Fails if the multimap is not empty. */
+ @CanIgnoreReturnValue
+ public Ordered containsExactly();
+
+ /**
+ * Fails if the multimap does not contain exactly the given set of key/value pairs.
+ *
+ * <p><b>Warning:</b> the use of varargs means that we cannot guarantee an equal number of
+ * key/value pairs at compile time. Please make sure you provide varargs in key/value pairs!
+ */
+ @CanIgnoreReturnValue
+ public Ordered containsExactly(@Nullable Object k0, @Nullable M v0, /*@Nullable*/ Object... rest);
+
+ /**
+ * @deprecated Do not call {@code equals()} on a {@code MultimapWithProtoValuesFluentAssertion}.
+ * @see com.google.common.truth.Subject#equals(Object)
+ */
+ @Override
+ @Deprecated
+ boolean equals(Object o);
+
+ /**
+ * @deprecated {@code MultimapWithProtoValuesFluentAssertion} does not support {@code hashCode()}.
+ * @see com.google.common.truth.Subject#hashCode()
+ */
+ @Override
+ @Deprecated
+ int hashCode();
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/MultimapWithProtoValuesSubject.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/MultimapWithProtoValuesSubject.java
new file mode 100644
index 00000000..8a1e5c0a
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/MultimapWithProtoValuesSubject.java
@@ -0,0 +1,943 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.Lists.asList;
+import static com.google.common.truth.extensions.proto.FieldScopeUtil.asList;
+import static com.google.common.truth.extensions.proto.ProtoTruth.protos;
+
+import com.google.common.collect.Multimap;
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.MultimapSubject;
+import com.google.common.truth.Ordered;
+import com.google.common.truth.Subject;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.ExtensionRegistry;
+import com.google.protobuf.Message;
+import com.google.protobuf.TypeRegistry;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Truth subject for {@link Multimap}s with protocol buffers for values.
+ *
+ * <p>{@code ProtoTruth.assertThat(actual).containsExactlyEntriesIn(expected)} performs the same
+ * assertion as {@code Truth.assertThat(actual).containsExactlyEntriesIn(expected)}. By default, the
+ * assertions are strict with respect to repeated field order, missing fields, etc. This behavior
+ * can be changed with the configuration methods on this subject, e.g. {@code
+ * ProtoTruth.assertThat(actual).ignoringRepeatedFieldOrder().containsExactlyEntriesIn(expected)}.
+ *
+ * <p>By default, floating-point fields are compared using exact equality, which is <a
+ * href="https://truth.dev/floating_point">probably not what you want</a> if the values are the
+ * results of some arithmetic. To check for approximate equality, use {@link
+ * #usingDoubleToleranceForValues}, {@link #usingFloatToleranceForValues}, and {@linkplain
+ * #usingDoubleToleranceForFieldsForValues(double, int, int...) their per-field equivalents}.
+ *
+ * <p>Equality tests, and other methods, may yield slightly different behavior for versions 2 and 3
+ * of Protocol Buffers. If testing protos of multiple versions, make sure you understand the
+ * behaviors of default and unknown fields so you don't under or over test.
+ *
+ * @param <M> the type of the message values in the multimap
+ */
+public class MultimapWithProtoValuesSubject<M extends Message> extends MultimapSubject {
+
+ /*
+ * Storing a FailureMetadata instance in a Subject subclass is generally a bad practice. For an
+ * explanation of why it works out OK here, see LiteProtoSubject.
+ */
+ private final FailureMetadata metadata;
+ private final Multimap<?, M> actual;
+ private final FluentEqualityConfig config;
+
+ protected MultimapWithProtoValuesSubject(
+ FailureMetadata failureMetadata, @Nullable Multimap<?, M> multimap) {
+ this(failureMetadata, FluentEqualityConfig.defaultInstance(), multimap);
+ }
+
+ MultimapWithProtoValuesSubject(
+ FailureMetadata failureMetadata,
+ FluentEqualityConfig config,
+ @Nullable Multimap<?, M> multimap) {
+ super(failureMetadata, multimap);
+ this.metadata = failureMetadata;
+ this.actual = multimap;
+ this.config = config;
+ }
+
+ /**
+ * Returns a context-aware {@link Subject} for making assertions about the values for the given
+ * key within the {@link Multimap}.
+ *
+ * <p>This method performs no checks on its own and cannot cause test failures. Subsequent
+ * assertions must be chained onto this method call to test properties of the {@link Multimap}.
+ */
+ public IterableOfProtosSubject<M> valuesForKey(@Nullable Object key) {
+ return check("valuesForKey(%s)", key)
+ .about(protos())
+ .that(((Multimap<Object, M>) actual).get(key));
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // MultimapWithProtoValuesFluentAssertion Configuration
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+
+ MultimapWithProtoValuesFluentAssertion<M> usingConfig(FluentEqualityConfig newConfig) {
+ return new MultimapWithProtoValuesFluentAssertionImpl<>(
+ new MultimapWithProtoValuesSubject<>(metadata, newConfig, actual));
+ }
+
+ /**
+ * Specifies that the 'has' bit of individual fields should be ignored when comparing for
+ * equality.
+ *
+ * <p>For version 2 Protocol Buffers, this setting determines whether two protos with the same
+ * value for a field compare equal if one explicitly sets the value, and the other merely
+ * implicitly uses the schema-defined default. This setting also determines whether unknown fields
+ * should be considered in the comparison. By {@code ignoringFieldAbsence()}, unknown fields are
+ * ignored, and value-equal fields as specified above are considered equal.
+ *
+ * <p>For version 3 Protocol Buffers, this setting does not affect primitive fields, because their
+ * default value is indistinguishable from unset.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceForValues() {
+ return usingConfig(config.ignoringFieldAbsence());
+ }
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified top-level field numbers should be
+ * ignored when comparing for equality. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if they are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsenceForValues()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsenceForValues() for details
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceOfFieldsForValues(
+ int firstFieldNumber, int... rest) {
+ return usingConfig(config.ignoringFieldAbsenceOfFields(asList(firstFieldNumber, rest)));
+ }
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified top-level field numbers should be
+ * ignored when comparing for equality. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if they are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsenceForValues()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsenceForValues() for details
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceOfFieldsForValues(
+ Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.ignoringFieldAbsenceOfFields(fieldNumbers));
+ }
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified field descriptors should be ignored
+ * when comparing for equality. Sub-fields must be specified explicitly if they are to be ignored
+ * as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsenceForValues()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsenceForValues() for details
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceOfFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return usingConfig(
+ config.ignoringFieldAbsenceOfFieldDescriptors(asList(firstFieldDescriptor, rest)));
+ }
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified field descriptors should be ignored
+ * when comparing for equality. Sub-fields must be specified explicitly if they are to be ignored
+ * as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsenceForValues()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsenceForValues() for details
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceOfFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(config.ignoringFieldAbsenceOfFieldDescriptors(fieldDescriptors));
+ }
+
+ /**
+ * Specifies that the ordering of repeated fields, at all levels, should be ignored when comparing
+ * for equality.
+ *
+ * <p>This setting applies to all repeated fields recursively, but it does not ignore structure.
+ * For example, with {@link #ignoringRepeatedFieldOrderForValues()}, a repeated {@code int32}
+ * field {@code bar}, set inside a repeated message field {@code foo}, the following protos will
+ * all compare equal:
+ *
+ * <pre>{@code
+ * message1: {
+ * foo: {
+ * bar: 1
+ * bar: 2
+ * }
+ * foo: {
+ * bar: 3
+ * bar: 4
+ * }
+ * }
+ *
+ * message2: {
+ * foo: {
+ * bar: 2
+ * bar: 1
+ * }
+ * foo: {
+ * bar: 4
+ * bar: 3
+ * }
+ * }
+ *
+ * message3: {
+ * foo: {
+ * bar: 4
+ * bar: 3
+ * }
+ * foo: {
+ * bar: 2
+ * bar: 1
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>However, the following message will compare equal to none of these:
+ *
+ * <pre>{@code
+ * message4: {
+ * foo: {
+ * bar: 1
+ * bar: 3
+ * }
+ * foo: {
+ * bar: 2
+ * bar: 4
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>This setting does not apply to map fields, for which field order is always ignored. The
+ * serialization order of map fields is undefined, and it may change from runtime to runtime.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderForValues() {
+ return usingConfig(config.ignoringRepeatedFieldOrder());
+ }
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified top-level field
+ * numbers should be ignored when comparing for equality. Sub-fields must be specified explicitly
+ * (via {@link FieldDescriptor}) if their orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrderForValues()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrderForValues() for details.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldsForValues(
+ int firstFieldNumber, int... rest) {
+ return usingConfig(config.ignoringRepeatedFieldOrderOfFields(asList(firstFieldNumber, rest)));
+ }
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified top-level field
+ * numbers should be ignored when comparing for equality. Sub-fields must be specified explicitly
+ * (via {@link FieldDescriptor}) if their orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrderForValues()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrderForValues() for details.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldsForValues(
+ Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.ignoringRepeatedFieldOrderOfFields(fieldNumbers));
+ }
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified field descriptors
+ * should be ignored when comparing for equality. Sub-fields must be specified explicitly if their
+ * orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrderForValues()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrderForValues() for details.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M>
+ ignoringRepeatedFieldOrderOfFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return usingConfig(
+ config.ignoringRepeatedFieldOrderOfFieldDescriptors(asList(firstFieldDescriptor, rest)));
+ }
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified field descriptors
+ * should be ignored when comparing for equality. Sub-fields must be specified explicitly if their
+ * orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrderForValues()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrderForValues() for details.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M>
+ ignoringRepeatedFieldOrderOfFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(config.ignoringRepeatedFieldOrderOfFieldDescriptors(fieldDescriptors));
+ }
+
+ /**
+ * Specifies that, for all repeated and map fields, any elements in the 'actual' proto which are
+ * not found in the 'expected' proto are ignored, with the exception of fields in the expected
+ * proto which are empty. To ignore empty repeated fields as well, use {@link
+ * #comparingExpectedFieldsOnlyForValues}.
+ *
+ * <p>This rule is applied independently from {@link #ignoringRepeatedFieldOrderForValues}. If
+ * ignoring repeated field order AND extra repeated field elements, all that is tested is that the
+ * expected elements comprise a subset of the actual elements. If not ignoring repeated field
+ * order, but still ignoring extra repeated field elements, the actual elements must contain a
+ * subsequence that matches the expected elements for the test to pass. (The subsequence rule does
+ * not apply to Map fields, which are always compared by key.)
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringExtraRepeatedFieldElementsForValues() {
+ return usingConfig(config.ignoringExtraRepeatedFieldElements());
+ }
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified top-level field
+ * numbers should be ignored. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if their extra elements are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElementsForValues()} instead to ignore these for all
+ * fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElementsForValues() for details.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M>
+ ignoringExtraRepeatedFieldElementsOfFieldsForValues(int firstFieldNumber, int... rest) {
+ return usingConfig(
+ config.ignoringExtraRepeatedFieldElementsOfFields(asList(firstFieldNumber, rest)));
+ }
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified top-level field
+ * numbers should be ignored. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if their extra elements are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElementsForValues()} instead to ignore these for all
+ * fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElementsForValues() for details.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M>
+ ignoringExtraRepeatedFieldElementsOfFieldsForValues(Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.ignoringExtraRepeatedFieldElementsOfFields(fieldNumbers));
+ }
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified field descriptors
+ * should be ignored. Sub-fields must be specified explicitly if their extra elements are to be
+ * ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElementsForValues()} instead to ignore these for all
+ * fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElementsForValues() for details.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M>
+ ignoringExtraRepeatedFieldElementsOfFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return usingConfig(
+ config.ignoringExtraRepeatedFieldElementsOfFieldDescriptors(
+ asList(firstFieldDescriptor, rest)));
+ }
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified field descriptors
+ * should be ignored. Sub-fields must be specified explicitly if their extra elements are to be
+ * ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElementsForValues()} instead to ignore these for all
+ * fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElementsForValues() for details.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M>
+ ignoringExtraRepeatedFieldElementsOfFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(
+ config.ignoringExtraRepeatedFieldElementsOfFieldDescriptors(fieldDescriptors));
+ }
+
+ /**
+ * Compares double fields as equal if they are both finite and their absolute difference is less
+ * than or equal to {@code tolerance}.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForValues(double tolerance) {
+ return usingConfig(config.usingDoubleTolerance(tolerance));
+ }
+
+ /**
+ * Compares double fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForFieldsForValues(
+ double tolerance, int firstFieldNumber, int... rest) {
+ return usingConfig(
+ config.usingDoubleToleranceForFields(tolerance, asList(firstFieldNumber, rest)));
+ }
+
+ /**
+ * Compares double fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForFieldsForValues(
+ double tolerance, Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.usingDoubleToleranceForFields(tolerance, fieldNumbers));
+ }
+
+ /**
+ * Compares double fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForFieldDescriptorsForValues(
+ double tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return usingConfig(
+ config.usingDoubleToleranceForFieldDescriptors(
+ tolerance, asList(firstFieldDescriptor, rest)));
+ }
+
+ /**
+ * Compares double fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForFieldDescriptorsForValues(
+ double tolerance, Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(config.usingDoubleToleranceForFieldDescriptors(tolerance, fieldDescriptors));
+ }
+
+ /**
+ * Compares float fields as equal if they are both finite and their absolute difference is less
+ * than or equal to {@code tolerance}.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> usingFloatToleranceForValues(float tolerance) {
+ return usingConfig(config.usingFloatTolerance(tolerance));
+ }
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> usingFloatToleranceForFieldsForValues(
+ float tolerance, int firstFieldNumber, int... rest) {
+ return usingConfig(
+ config.usingFloatToleranceForFields(tolerance, asList(firstFieldNumber, rest)));
+ }
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> usingFloatToleranceForFieldsForValues(
+ float tolerance, Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.usingFloatToleranceForFields(tolerance, fieldNumbers));
+ }
+
+ /**
+ * Compares float fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> usingFloatToleranceForFieldDescriptorsForValues(
+ float tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return usingConfig(
+ config.usingFloatToleranceForFieldDescriptors(
+ tolerance, asList(firstFieldDescriptor, rest)));
+ }
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> usingFloatToleranceForFieldDescriptorsForValues(
+ float tolerance, Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(config.usingFloatToleranceForFieldDescriptors(tolerance, fieldDescriptors));
+ }
+
+ /**
+ * Limits the comparison of Protocol buffers to the fields set in the expected proto(s). When
+ * multiple protos are specified, the comparison is limited to the union of set fields in all the
+ * expected protos.
+ *
+ * <p>The "expected proto(s)" are those passed to the method at the end of the call chain, such as
+ * {@link #containsEntry} or {@link #containsExactlyEntriesIn}.
+ *
+ * <p>Fields not set in the expected proto(s) are ignored. In particular, proto3 fields which have
+ * their default values are ignored, as these are indistinguishable from unset fields. If you want
+ * to assert that a proto3 message has certain fields with default values, you cannot use this
+ * method.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> comparingExpectedFieldsOnlyForValues() {
+ return usingConfig(config.comparingExpectedFieldsOnly());
+ }
+
+ /**
+ * Limits the comparison of Protocol buffers to the defined {@link FieldScope}.
+ *
+ * <p>This method is additive and has well-defined ordering semantics. If the invoking {@link
+ * ProtoFluentAssertion} is already scoped to a {@link FieldScope} {@code X}, and this method is
+ * invoked with {@link FieldScope} {@code Y}, the resultant {@link ProtoFluentAssertion} is
+ * constrained to the intersection of {@link FieldScope}s {@code X} and {@code Y}.
+ *
+ * <p>By default, {@link MultimapWithProtoValuesFluentAssertion} is constrained to {@link
+ * FieldScopes#all()}, that is, no fields are excluded from comparison.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> withPartialScopeForValues(
+ FieldScope fieldScope) {
+ return usingConfig(config.withPartialScope(checkNotNull(fieldScope, "fieldScope")));
+ }
+
+ /**
+ * Excludes the top-level message fields with the given tag numbers from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * numbers are ignored, and all sub-messages of type {@code M} will also have these field numbers
+ * ignored.
+ *
+ * <p>If an invalid field number is supplied, the terminal comparison operation will throw a
+ * runtime exception.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringFieldsForValues(
+ int firstFieldNumber, int... rest) {
+ return ignoringFieldsForValues(asList(firstFieldNumber, rest));
+ }
+
+ /**
+ * Excludes the top-level message fields with the given tag numbers from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * numbers are ignored, and all sub-messages of type {@code M} will also have these field numbers
+ * ignored.
+ *
+ * <p>If an invalid field number is supplied, the terminal comparison operation will throw a
+ * runtime exception.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringFieldsForValues(
+ Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.ignoringFields(fieldNumbers));
+ }
+
+ /**
+ * Excludes all message fields matching the given {@link FieldDescriptor}s from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * descriptors are ignored, no matter where they occur in the tree.
+ *
+ * <p>If a field descriptor which does not, or cannot occur in the proto structure is supplied, it
+ * is silently ignored.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return ignoringFieldDescriptorsForValues(asList(firstFieldDescriptor, rest));
+ }
+
+ /**
+ * Excludes all message fields matching the given {@link FieldDescriptor}s from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * descriptors are ignored, no matter where they occur in the tree.
+ *
+ * <p>If a field descriptor which does not, or cannot occur in the proto structure is supplied, it
+ * is silently ignored.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(config.ignoringFieldDescriptors(fieldDescriptors));
+ }
+
+ /**
+ * Excludes all specific field paths under the argument {@link FieldScope} from the comparison.
+ *
+ * <p>This method is additive and has well-defined ordering semantics. If the invoking {@link
+ * ProtoFluentAssertion} is already scoped to a {@link FieldScope} {@code X}, and this method is
+ * invoked with {@link FieldScope} {@code Y}, the resultant {@link ProtoFluentAssertion} is
+ * constrained to the subtraction of {@code X - Y}.
+ *
+ * <p>By default, {@link ProtoFluentAssertion} is constrained to {@link FieldScopes#all()}, that
+ * is, no fields are excluded from comparison.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringFieldScopeForValues(
+ FieldScope fieldScope) {
+ return usingConfig(config.ignoringFieldScope(checkNotNull(fieldScope, "fieldScope")));
+ }
+
+ /**
+ * If set, in the event of a comparison failure, the error message printed will list only those
+ * specific fields that did not match between the actual and expected values. Useful for very
+ * large protocol buffers.
+ *
+ * <p>This a purely cosmetic setting, and it has no effect on the behavior of the test.
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> reportingMismatchesOnlyForValues() {
+ return usingConfig(config.reportingMismatchesOnly());
+ }
+
+ /**
+ * Specifies the {@link TypeRegistry} and {@link ExtensionRegistry} to use for {@link
+ * com.google.protobuf.Any Any} messages.
+ *
+ * <p>To compare the value of an {@code Any} message, ProtoTruth looks in the given type registry
+ * for a descriptor for the message's type URL:
+ *
+ * <ul>
+ * <li>If ProtoTruth finds a descriptor, it unpacks the value and compares it against the
+ * expected value, respecting any configuration methods used for the assertion.
+ * <li>If ProtoTruth does not find a descriptor (or if the value can't be deserialized with the
+ * descriptor), it compares the raw, serialized bytes of the expected and actual values.
+ * </ul>
+ *
+ * <p>When ProtoTruth unpacks a value, it is parsing a serialized proto. That proto may contain
+ * extensions. To look up those extensions, ProtoTruth uses the provided {@link
+ * ExtensionRegistry}.
+ *
+ * @since 1.1
+ */
+ public MultimapWithProtoValuesFluentAssertion<M> unpackingAnyUsingForValues(
+ TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry) {
+ return usingConfig(config.unpackingAnyUsing(typeRegistry, extensionRegistry));
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // UsingCorrespondence Methods
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+
+ private MultimapSubject.UsingCorrespondence<M, M> usingCorrespondence(
+ Iterable<? extends M> expectedValues) {
+ return comparingValuesUsing(
+ config
+ .withExpectedMessages(expectedValues)
+ .<M>toCorrespondence(FieldScopeUtil.getSingleDescriptor(actual.values())));
+ }
+
+ // The UsingCorrespondence methods have conflicting erasure with default MapSubject methods,
+ // so we can't implement them both on the same class, but we want to define both so
+ // MultimapWithProtoValuesSubjects are interchangeable with MapSubjects when no configuration is
+ // specified. So, we implement a dumb, private delegator to return instead.
+ private static final class MultimapWithProtoValuesFluentAssertionImpl<M extends Message>
+ implements MultimapWithProtoValuesFluentAssertion<M> {
+ private final MultimapWithProtoValuesSubject<M> subject;
+
+ MultimapWithProtoValuesFluentAssertionImpl(MultimapWithProtoValuesSubject<M> subject) {
+ this.subject = subject;
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceForValues() {
+ return subject.ignoringFieldAbsenceForValues();
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceOfFieldsForValues(
+ int firstFieldNumber, int... rest) {
+ return subject.ignoringFieldAbsenceOfFieldsForValues(firstFieldNumber, rest);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringFieldAbsenceOfFieldsForValues(
+ Iterable<Integer> fieldNumbers) {
+ return subject.ignoringFieldAbsenceOfFieldsForValues(fieldNumbers);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M>
+ ignoringFieldAbsenceOfFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return subject.ignoringFieldAbsenceOfFieldDescriptorsForValues(firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M>
+ ignoringFieldAbsenceOfFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return subject.ignoringFieldAbsenceOfFieldDescriptorsForValues(fieldDescriptors);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderForValues() {
+ return subject.ignoringRepeatedFieldOrderForValues();
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldsForValues(
+ int firstFieldNumber, int... rest) {
+ return subject.ignoringRepeatedFieldOrderOfFieldsForValues(firstFieldNumber, rest);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringRepeatedFieldOrderOfFieldsForValues(
+ Iterable<Integer> fieldNumbers) {
+ return subject.ignoringRepeatedFieldOrderOfFieldsForValues(fieldNumbers);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M>
+ ignoringRepeatedFieldOrderOfFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return subject.ignoringRepeatedFieldOrderOfFieldDescriptorsForValues(
+ firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M>
+ ignoringRepeatedFieldOrderOfFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return subject.ignoringRepeatedFieldOrderOfFieldDescriptorsForValues(fieldDescriptors);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringExtraRepeatedFieldElementsForValues() {
+ return subject.ignoringExtraRepeatedFieldElementsForValues();
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M>
+ ignoringExtraRepeatedFieldElementsOfFieldsForValues(int firstFieldNumber, int... rest) {
+ return subject.ignoringExtraRepeatedFieldElementsOfFieldsForValues(firstFieldNumber, rest);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M>
+ ignoringExtraRepeatedFieldElementsOfFieldsForValues(Iterable<Integer> fieldNumbers) {
+ return subject.ignoringExtraRepeatedFieldElementsOfFieldsForValues(fieldNumbers);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M>
+ ignoringExtraRepeatedFieldElementsOfFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return subject.ignoringExtraRepeatedFieldElementsOfFieldDescriptorsForValues(
+ firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M>
+ ignoringExtraRepeatedFieldElementsOfFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return subject.ignoringExtraRepeatedFieldElementsOfFieldDescriptorsForValues(
+ fieldDescriptors);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForValues(
+ double tolerance) {
+ return subject.usingDoubleToleranceForValues(tolerance);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForFieldsForValues(
+ double tolerance, int firstFieldNumber, int... rest) {
+ return subject.usingDoubleToleranceForFieldsForValues(tolerance, firstFieldNumber, rest);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M> usingDoubleToleranceForFieldsForValues(
+ double tolerance, Iterable<Integer> fieldNumbers) {
+ return subject.usingDoubleToleranceForFieldsForValues(tolerance, fieldNumbers);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M>
+ usingDoubleToleranceForFieldDescriptorsForValues(
+ double tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return subject.usingDoubleToleranceForFieldDescriptorsForValues(
+ tolerance, firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M>
+ usingDoubleToleranceForFieldDescriptorsForValues(
+ double tolerance, Iterable<FieldDescriptor> fieldDescriptors) {
+ return subject.usingDoubleToleranceForFieldDescriptorsForValues(tolerance, fieldDescriptors);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M> usingFloatToleranceForValues(float tolerance) {
+ return subject.usingFloatToleranceForValues(tolerance);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M> usingFloatToleranceForFieldsForValues(
+ float tolerance, int firstFieldNumber, int... rest) {
+ return subject.usingFloatToleranceForFieldsForValues(tolerance, firstFieldNumber, rest);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M> usingFloatToleranceForFieldsForValues(
+ float tolerance, Iterable<Integer> fieldNumbers) {
+ return subject.usingFloatToleranceForFieldsForValues(tolerance, fieldNumbers);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M>
+ usingFloatToleranceForFieldDescriptorsForValues(
+ float tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return subject.usingFloatToleranceForFieldDescriptorsForValues(
+ tolerance, firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M>
+ usingFloatToleranceForFieldDescriptorsForValues(
+ float tolerance, Iterable<FieldDescriptor> fieldDescriptors) {
+ return subject.usingFloatToleranceForFieldDescriptorsForValues(tolerance, fieldDescriptors);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M> comparingExpectedFieldsOnlyForValues() {
+ return subject.comparingExpectedFieldsOnlyForValues();
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M> withPartialScopeForValues(
+ FieldScope fieldScope) {
+ return subject.withPartialScopeForValues(fieldScope);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringFieldsForValues(
+ int firstFieldNumber, int... rest) {
+ return subject.ignoringFieldsForValues(firstFieldNumber, rest);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringFieldsForValues(
+ Iterable<Integer> fieldNumbers) {
+ return subject.ignoringFieldsForValues(fieldNumbers);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringFieldDescriptorsForValues(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return subject.ignoringFieldDescriptorsForValues(firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringFieldDescriptorsForValues(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return subject.ignoringFieldDescriptorsForValues(fieldDescriptors);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M> ignoringFieldScopeForValues(
+ FieldScope fieldScope) {
+ return subject.ignoringFieldScopeForValues(fieldScope);
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M> reportingMismatchesOnlyForValues() {
+ return subject.reportingMismatchesOnlyForValues();
+ }
+
+ @Override
+ public MultimapWithProtoValuesFluentAssertion<M> unpackingAnyUsingForValues(
+ TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry) {
+ return subject.unpackingAnyUsingForValues(typeRegistry, extensionRegistry);
+ }
+
+ @Override
+ public void containsEntry(@Nullable Object expectedKey, @Nullable M expectedValue) {
+ subject
+ .usingCorrespondence(Arrays.asList(expectedValue))
+ .containsEntry(expectedKey, expectedValue);
+ }
+
+ @Override
+ public void doesNotContainEntry(@Nullable Object excludedKey, @Nullable M excludedValue) {
+ subject
+ .usingCorrespondence(Arrays.asList(excludedValue))
+ .doesNotContainEntry(excludedKey, excludedValue);
+ }
+
+ @Override
+ @CanIgnoreReturnValue
+ public Ordered containsExactlyEntriesIn(Multimap<?, ? extends M> expectedMap) {
+ return subject
+ .usingCorrespondence(expectedMap.values())
+ .containsExactlyEntriesIn(expectedMap);
+ }
+
+ @Override
+ @CanIgnoreReturnValue
+ public Ordered containsExactly() {
+ return subject.usingCorrespondence(Collections.<M>emptyList()).containsExactly();
+ }
+
+ @Override
+ @CanIgnoreReturnValue
+ @SuppressWarnings("unchecked") // ClassCastException is fine
+ public Ordered containsExactly(
+ @Nullable Object k0, @Nullable M v0, /*@Nullable*/ Object... rest) {
+ List<M> expectedValues = new ArrayList<>();
+ expectedValues.add(v0);
+ for (int i = 1; i < rest.length; i += 2) {
+ expectedValues.add((M) rest[i]);
+ }
+ return subject.usingCorrespondence(expectedValues).containsExactly(k0, v0, rest);
+ }
+
+ @SuppressWarnings("DoNotCall")
+ @Override
+ @Deprecated
+ public boolean equals(Object o) {
+ return subject.equals(o);
+ }
+
+ @SuppressWarnings("DoNotCall")
+ @Override
+ @Deprecated
+ public int hashCode() {
+ return subject.hashCode();
+ }
+ }
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoFluentAssertion.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoFluentAssertion.java
new file mode 100644
index 00000000..3047caba
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoFluentAssertion.java
@@ -0,0 +1,512 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.ExtensionRegistry;
+import com.google.protobuf.Message;
+import com.google.protobuf.TypeRegistry;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Fluent API to perform detailed, customizable comparison of Protocol buffers.
+ *
+ * <p>Methods may be chained in any order, but the chain should terminate with {@link
+ * #isEqualTo(Message)} or {@link #isNotEqualTo(Message)}.
+ *
+ * <p>The state of a {@code ProtoFluentAssertion} object after each method is called is left
+ * undefined. Users should not retain references to {@code ProtoFluentAssertion} instances.
+ */
+public interface ProtoFluentAssertion {
+
+ /**
+ * Specifies that the 'has' bit of individual fields should be ignored when comparing for
+ * equality.
+ *
+ * <p>For version 2 Protocol Buffers, this setting determines whether two protos with the same
+ * value for a field compare equal if one explicitly sets the value, and the other merely
+ * implicitly uses the schema-defined default. This setting also determines whether unknown fields
+ * should be considered in the comparison. By {@code ignoringFieldAbsence()}, unknown fields are
+ * ignored, and value-equal fields as specified above are considered equal.
+ *
+ * <p>For version 3 Protocol Buffers, this setting does not affect primitive fields, because their
+ * default value is indistinguishable from unset.
+ */
+ ProtoFluentAssertion ignoringFieldAbsence();
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified top-level field numbers should be
+ * ignored when comparing for equality. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if they are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsence()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsence() for details
+ */
+ ProtoFluentAssertion ignoringFieldAbsenceOfFields(int firstFieldNumber, int... rest);
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified top-level field numbers should be
+ * ignored when comparing for equality. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if they are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsence()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsence() for details
+ */
+ ProtoFluentAssertion ignoringFieldAbsenceOfFields(Iterable<Integer> fieldNumbers);
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified field descriptors should be ignored
+ * when comparing for equality. Sub-fields must be specified explicitly if they are to be ignored
+ * as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsence()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsence() for details
+ */
+ ProtoFluentAssertion ignoringFieldAbsenceOfFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified field descriptors should be ignored
+ * when comparing for equality. Sub-fields must be specified explicitly if they are to be ignored
+ * as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsence()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsence() for details
+ */
+ ProtoFluentAssertion ignoringFieldAbsenceOfFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Specifies that the ordering of repeated fields, at all levels, should be ignored when comparing
+ * for equality.
+ *
+ * <p>This setting applies to all repeated fields recursively, but it does not ignore structure.
+ * For example, with {@link #ignoringRepeatedFieldOrder()}, a repeated {@code int32} field {@code
+ * bar}, set inside a repeated message field {@code foo}, the following protos will all compare
+ * equal:
+ *
+ * <pre>{@code
+ * message1: {
+ * foo: {
+ * bar: 1
+ * bar: 2
+ * }
+ * foo: {
+ * bar: 3
+ * bar: 4
+ * }
+ * }
+ *
+ * message2: {
+ * foo: {
+ * bar: 2
+ * bar: 1
+ * }
+ * foo: {
+ * bar: 4
+ * bar: 3
+ * }
+ * }
+ *
+ * message3: {
+ * foo: {
+ * bar: 4
+ * bar: 3
+ * }
+ * foo: {
+ * bar: 2
+ * bar: 1
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>However, the following message will compare equal to none of these:
+ *
+ * <pre>{@code
+ * message4: {
+ * foo: {
+ * bar: 1
+ * bar: 3
+ * }
+ * foo: {
+ * bar: 2
+ * bar: 4
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>This setting does not apply to map fields, for which field order is always ignored. The
+ * serialization order of map fields is undefined, and it may change from runtime to runtime.
+ */
+ ProtoFluentAssertion ignoringRepeatedFieldOrder();
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified top-level field
+ * numbers should be ignored when comparing for equality. Sub-fields must be specified explicitly
+ * (via {@link FieldDescriptor}) if their orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrder()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrder() for details.
+ */
+ ProtoFluentAssertion ignoringRepeatedFieldOrderOfFields(int firstFieldNumber, int... rest);
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified top-level field
+ * numbers should be ignored when comparing for equality. Sub-fields must be specified explicitly
+ * (via {@link FieldDescriptor}) if their orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrder()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrder() for details.
+ */
+ ProtoFluentAssertion ignoringRepeatedFieldOrderOfFields(Iterable<Integer> fieldNumbers);
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified field descriptors
+ * should be ignored when comparing for equality. Sub-fields must be specified explicitly if their
+ * orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrder()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrder() for details.
+ */
+ ProtoFluentAssertion ignoringRepeatedFieldOrderOfFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified field descriptors
+ * should be ignored when comparing for equality. Sub-fields must be specified explicitly if their
+ * orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrder()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrder() for details.
+ */
+ ProtoFluentAssertion ignoringRepeatedFieldOrderOfFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Specifies that, for all repeated and map fields, any elements in the 'actual' proto which are
+ * not found in the 'expected' proto are ignored, with the exception of fields in the expected
+ * proto which are empty. To ignore empty repeated fields as well, use {@link
+ * #comparingExpectedFieldsOnly}.
+ *
+ * <p>This rule is applied independently from {@link #ignoringRepeatedFieldOrder}. If ignoring
+ * repeated field order AND extra repeated field elements, all that is tested is that the expected
+ * elements comprise a subset of the actual elements. If not ignoring repeated field order, but
+ * still ignoring extra repeated field elements, the actual elements must contain a subsequence
+ * that matches the expected elements for the test to pass. (The subsequence rule does not apply
+ * to Map fields, which are always compared by key.)
+ */
+ ProtoFluentAssertion ignoringExtraRepeatedFieldElements();
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified top-level field
+ * numbers should be ignored. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if their extra elements are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElements()} instead to ignore these for all fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElements() for details.
+ */
+ ProtoFluentAssertion ignoringExtraRepeatedFieldElementsOfFields(
+ int firstFieldNumber, int... rest);
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified top-level field
+ * numbers should be ignored. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if their extra elements are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElements()} instead to ignore these for all fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElements() for details.
+ */
+ ProtoFluentAssertion ignoringExtraRepeatedFieldElementsOfFields(Iterable<Integer> fieldNumbers);
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified field descriptors
+ * should be ignored. Sub-fields must be specified explicitly if their extra elements are to be
+ * ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElements()} instead to ignore these for all fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElements() for details.
+ */
+ ProtoFluentAssertion ignoringExtraRepeatedFieldElementsOfFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified field descriptors
+ * should be ignored. Sub-fields must be specified explicitly if their extra elements are to be
+ * ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElements()} instead to ignore these for all fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElements() for details.
+ */
+ ProtoFluentAssertion ignoringExtraRepeatedFieldElementsOfFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Compares double fields as equal if they are both finite and their absolute difference is less
+ * than or equal to {@code tolerance}.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ ProtoFluentAssertion usingDoubleTolerance(double tolerance);
+
+ /**
+ * Compares double fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ ProtoFluentAssertion usingDoubleToleranceForFields(
+ double tolerance, int firstFieldNumber, int... rest);
+
+ /**
+ * Compares double fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ ProtoFluentAssertion usingDoubleToleranceForFields(
+ double tolerance, Iterable<Integer> fieldNumbers);
+
+ /**
+ * Compares double fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ ProtoFluentAssertion usingDoubleToleranceForFieldDescriptors(
+ double tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Compares double fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ ProtoFluentAssertion usingDoubleToleranceForFieldDescriptors(
+ double tolerance, Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Compares float fields as equal if they are both finite and their absolute difference is less
+ * than or equal to {@code tolerance}.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ ProtoFluentAssertion usingFloatTolerance(float tolerance);
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ ProtoFluentAssertion usingFloatToleranceForFields(
+ float tolerance, int firstFieldNumber, int... rest);
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ ProtoFluentAssertion usingFloatToleranceForFields(
+ float tolerance, Iterable<Integer> fieldNumbers);
+
+ /**
+ * Compares float fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ ProtoFluentAssertion usingFloatToleranceForFieldDescriptors(
+ float tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ ProtoFluentAssertion usingFloatToleranceForFieldDescriptors(
+ float tolerance, Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Limits the comparison of Protocol buffers to the fields set in the expected proto(s). When
+ * multiple protos are specified, the comparison is limited to the union of set fields in all the
+ * expected protos.
+ *
+ * <p>The "expected proto(s)" are those passed to the void method at the end of the {@code
+ * ProtoFluentAssertion} call-chain: For example, {@link #isEqualTo(Message)}, or {@link
+ * #isNotEqualTo(Message)}.
+ *
+ * <p>Fields not set in the expected proto(s) are ignored. In particular, proto3 fields which have
+ * their default values are ignored, as these are indistinguishable from unset fields. If you want
+ * to assert that a proto3 message has certain fields with default values, you cannot use this
+ * method.
+ */
+ ProtoFluentAssertion comparingExpectedFieldsOnly();
+
+ /**
+ * Limits the comparison of Protocol buffers to the defined {@link FieldScope}.
+ *
+ * <p>This method is additive and has well-defined ordering semantics. If the invoking {@link
+ * ProtoFluentAssertion} is already scoped to a {@link FieldScope} {@code X}, and this method is
+ * invoked with {@link FieldScope} {@code Y}, the resultant {@link ProtoFluentAssertion} is
+ * constrained to the intersection of {@link FieldScope}s {@code X} and {@code Y}.
+ *
+ * <p>By default, {@link ProtoFluentAssertion} is constrained to {@link FieldScopes#all()}, that
+ * is, no fields are excluded from comparison.
+ */
+ ProtoFluentAssertion withPartialScope(FieldScope fieldScope);
+
+ /**
+ * Excludes the top-level message fields with the given tag numbers from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * numbers are ignored, and all sub-messages of type {@code M} will also have these field numbers
+ * ignored.
+ *
+ * <p>If an invalid field number is supplied, the terminal comparison operation will throw a
+ * runtime exception.
+ */
+ ProtoFluentAssertion ignoringFields(int firstFieldNumber, int... rest);
+
+ /**
+ * Excludes the top-level message fields with the given tag numbers from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * numbers are ignored, and all sub-messages of type {@code M} will also have these field numbers
+ * ignored.
+ *
+ * <p>If an invalid field number is supplied, the terminal comparison operation will throw a
+ * runtime exception.
+ */
+ ProtoFluentAssertion ignoringFields(Iterable<Integer> fieldNumbers);
+
+ /**
+ * Excludes all message fields matching the given {@link FieldDescriptor}s from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * descriptors are ignored, no matter where they occur in the tree.
+ *
+ * <p>If a field descriptor which does not, or cannot occur in the proto structure is supplied, it
+ * is silently ignored.
+ */
+ ProtoFluentAssertion ignoringFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest);
+
+ /**
+ * Excludes all message fields matching the given {@link FieldDescriptor}s from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * descriptors are ignored, no matter where they occur in the tree.
+ *
+ * <p>If a field descriptor which does not, or cannot occur in the proto structure is supplied, it
+ * is silently ignored.
+ */
+ ProtoFluentAssertion ignoringFieldDescriptors(Iterable<FieldDescriptor> fieldDescriptors);
+
+ /**
+ * Excludes all specific field paths under the argument {@link FieldScope} from the comparison.
+ *
+ * <p>This method is additive and has well-defined ordering semantics. If the invoking {@link
+ * ProtoFluentAssertion} is already scoped to a {@link FieldScope} {@code X}, and this method is
+ * invoked with {@link FieldScope} {@code Y}, the resultant {@link ProtoFluentAssertion} is
+ * constrained to the subtraction of {@code X - Y}.
+ *
+ * <p>By default, {@link ProtoFluentAssertion} is constrained to {@link FieldScopes#all()}, that
+ * is, no fields are excluded from comparison.
+ */
+ ProtoFluentAssertion ignoringFieldScope(FieldScope fieldScope);
+
+ /**
+ * If set, in the event of a comparison failure, the error message printed will list only those
+ * specific fields that did not match between the actual and expected values. Useful for very
+ * large protocol buffers.
+ *
+ * <p>This a purely cosmetic setting, and it has no effect on the behavior of the test.
+ */
+ ProtoFluentAssertion reportingMismatchesOnly();
+
+ /**
+ * Specifies the {@link TypeRegistry} and {@link ExtensionRegistry} to use for {@link
+ * com.google.protobuf.Any Any} messages.
+ *
+ * <p>To compare the value of an {@code Any} message, ProtoTruth looks in the given type registry
+ * for a descriptor for the message's type URL:
+ *
+ * <ul>
+ * <li>If ProtoTruth finds a descriptor, it unpacks the value and compares it against the
+ * expected value, respecting any configuration methods used for the assertion.
+ * <li>If ProtoTruth does not find a descriptor (or if the value can't be deserialized with the
+ * descriptor), it compares the raw, serialized bytes of the expected and actual values.
+ * </ul>
+ *
+ * <p>When ProtoTruth unpacks a value, it is parsing a serialized proto. That proto may contain
+ * extensions. To look up those extensions, ProtoTruth uses the provided {@link
+ * ExtensionRegistry}.
+ *
+ * @since 1.1
+ */
+ ProtoFluentAssertion unpackingAnyUsing(
+ TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry);
+
+ /**
+ * Compares the subject of the assertion to {@code expected}, using all of the rules specified by
+ * earlier operations. If no settings are changed, this invokes the default {@code equals}
+ * implementation of the subject {@link Message}.
+ */
+ void isEqualTo(@Nullable Message expected);
+
+ /**
+ * Compares the subject of the assertion to {@code expected}, expecting a difference, using all of
+ * the rules specified by earlier operations. If no settings are changed, this invokes the default
+ * {@code equals} implementation of the subject {@link Message}.
+ */
+ void isNotEqualTo(@Nullable Message expected);
+
+ /**
+ * @deprecated Do not call {@code equals()} on a {@code ProtoFluentAssertion}. Use {@link
+ * #isEqualTo(Message)} instead.
+ * @see com.google.common.truth.Subject#equals(Object)
+ */
+ @Override
+ @Deprecated
+ boolean equals(Object o);
+
+ /**
+ * @deprecated {@code ProtoFluentAssertion} does not support {@code hashCode()}. Use {@link
+ * #isEqualTo(Message)} for testing.
+ * @see com.google.common.truth.Subject#hashCode()
+ */
+ @Override
+ @Deprecated
+ int hashCode();
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoSubject.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoSubject.java
new file mode 100644
index 00000000..ba96333d
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoSubject.java
@@ -0,0 +1,907 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.Lists.asList;
+import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.Fact.simpleFact;
+import static com.google.common.truth.extensions.proto.FieldScopeUtil.asList;
+
+import com.google.common.base.Objects;
+import com.google.common.truth.FailureMetadata;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.ExtensionRegistry;
+import com.google.protobuf.Message;
+import com.google.protobuf.TypeRegistry;
+import java.util.Arrays;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Truth subject for the full version of Protocol Buffers.
+ *
+ * <p>{@code ProtoTruth.assertThat(actual).isEqualTo(expected)} performs the same assertion as
+ * {@code Truth.assertThat(actual).isEqualTo(expected)}, but with a better failure message. By
+ * default, the assertions are strict with respect to repeated field order, missing fields, etc.
+ * This behavior can be changed with the configuration methods on this subject, e.g. {@code
+ * ProtoTruth.assertThat(actual).ignoringRepeatedFieldOrder().isEqualTo(expected)}.
+ *
+ * <p>By default, floating-point fields are compared using exact equality, which is <a
+ * href="https://truth.dev/floating_point">probably not what you want</a> if the values are the
+ * results of some arithmetic. To check for approximate equality, use {@link #usingDoubleTolerance},
+ * {@link #usingFloatTolerance}, and {@linkplain #usingDoubleToleranceForFields(double, int, int...)
+ * their per-field equivalents}.
+ *
+ * <p>Equality tests, and other methods, may yield slightly different behavior for versions 2 and 3
+ * of Protocol Buffers. If testing protos of multiple versions, make sure you understand the
+ * behaviors of default and unknown fields so you don't under or over test.
+ */
+public class ProtoSubject extends LiteProtoSubject {
+
+ /*
+ * Storing a FailureMetadata instance in a Subject subclass is generally a bad practice. For an
+ * explanation of why it works out OK here, see LiteProtoSubject.
+ */
+ private final FailureMetadata metadata;
+ private final Message actual;
+ private final FluentEqualityConfig config;
+
+ protected ProtoSubject(FailureMetadata failureMetadata, @Nullable Message message) {
+ this(failureMetadata, FluentEqualityConfig.defaultInstance(), message);
+ }
+
+ ProtoSubject(
+ FailureMetadata failureMetadata, FluentEqualityConfig config, @Nullable Message message) {
+ super(failureMetadata, message);
+ this.metadata = failureMetadata;
+ this.actual = message;
+ this.config = config;
+ }
+
+ ProtoFluentAssertionImpl usingConfig(FluentEqualityConfig newConfig) {
+ return new ProtoFluentAssertionImpl(new ProtoSubject(metadata, newConfig, actual));
+ }
+
+ /**
+ * Specifies that the 'has' bit of individual fields should be ignored when comparing for
+ * equality.
+ *
+ * <p>For version 2 Protocol Buffers, this setting determines whether two protos with the same
+ * value for a field compare equal if one explicitly sets the value, and the other merely
+ * implicitly uses the schema-defined default. This setting also determines whether unknown fields
+ * should be considered in the comparison. By {@code ignoringFieldAbsence()}, unknown fields are
+ * ignored, and value-equal fields as specified above are considered equal.
+ *
+ * <p>For version 3 Protocol Buffers, this setting does not affect primitive fields, because their
+ * default value is indistinguishable from unset.
+ */
+ public ProtoFluentAssertion ignoringFieldAbsence() {
+ return usingConfig(config.ignoringFieldAbsence());
+ }
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified top-level field numbers should be
+ * ignored when comparing for equality. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if they are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsence()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsence() for details
+ */
+ public ProtoFluentAssertion ignoringFieldAbsenceOfFields(int firstFieldNumber, int... rest) {
+ return usingConfig(config.ignoringFieldAbsenceOfFields(asList(firstFieldNumber, rest)));
+ }
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified top-level field numbers should be
+ * ignored when comparing for equality. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if they are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsence()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsence() for details
+ */
+ public ProtoFluentAssertion ignoringFieldAbsenceOfFields(Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.ignoringFieldAbsenceOfFields(fieldNumbers));
+ }
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified field descriptors should be ignored
+ * when comparing for equality. Sub-fields must be specified explicitly if they are to be ignored
+ * as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsence()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsence() for details
+ */
+ public ProtoFluentAssertion ignoringFieldAbsenceOfFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return usingConfig(
+ config.ignoringFieldAbsenceOfFieldDescriptors(asList(firstFieldDescriptor, rest)));
+ }
+
+ /**
+ * Specifies that the 'has' bit of these explicitly specified field descriptors should be ignored
+ * when comparing for equality. Sub-fields must be specified explicitly if they are to be ignored
+ * as well.
+ *
+ * <p>Use {@link #ignoringFieldAbsence()} instead to ignore the 'has' bit for all fields.
+ *
+ * @see #ignoringFieldAbsence() for details
+ */
+ public ProtoFluentAssertion ignoringFieldAbsenceOfFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(config.ignoringFieldAbsenceOfFieldDescriptors(fieldDescriptors));
+ }
+
+ /**
+ * Specifies that the ordering of repeated fields, at all levels, should be ignored when comparing
+ * for equality.
+ *
+ * <p>This setting applies to all repeated fields recursively, but it does not ignore structure.
+ * For example, with {@link #ignoringRepeatedFieldOrder()}, a repeated {@code int32} field {@code
+ * bar}, set inside a repeated message field {@code foo}, the following protos will all compare
+ * equal:
+ *
+ * <pre>{@code
+ * message1: {
+ * foo: {
+ * bar: 1
+ * bar: 2
+ * }
+ * foo: {
+ * bar: 3
+ * bar: 4
+ * }
+ * }
+ *
+ * message2: {
+ * foo: {
+ * bar: 2
+ * bar: 1
+ * }
+ * foo: {
+ * bar: 4
+ * bar: 3
+ * }
+ * }
+ *
+ * message3: {
+ * foo: {
+ * bar: 4
+ * bar: 3
+ * }
+ * foo: {
+ * bar: 2
+ * bar: 1
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>However, the following message will compare equal to none of these:
+ *
+ * <pre>{@code
+ * message4: {
+ * foo: {
+ * bar: 1
+ * bar: 3
+ * }
+ * foo: {
+ * bar: 2
+ * bar: 4
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>This setting does not apply to map fields, for which field order is always ignored. The
+ * serialization order of map fields is undefined, and it may change from runtime to runtime.
+ */
+ public ProtoFluentAssertion ignoringRepeatedFieldOrder() {
+ return usingConfig(config.ignoringRepeatedFieldOrder());
+ }
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified top-level field
+ * numbers should be ignored when comparing for equality. Sub-fields must be specified explicitly
+ * (via {@link FieldDescriptor}) if their orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrder()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrder() for details.
+ */
+ public ProtoFluentAssertion ignoringRepeatedFieldOrderOfFields(
+ int firstFieldNumber, int... rest) {
+ return usingConfig(config.ignoringRepeatedFieldOrderOfFields(asList(firstFieldNumber, rest)));
+ }
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified top-level field
+ * numbers should be ignored when comparing for equality. Sub-fields must be specified explicitly
+ * (via {@link FieldDescriptor}) if their orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrder()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrder() for details.
+ */
+ public ProtoFluentAssertion ignoringRepeatedFieldOrderOfFields(Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.ignoringRepeatedFieldOrderOfFields(fieldNumbers));
+ }
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified field descriptors
+ * should be ignored when comparing for equality. Sub-fields must be specified explicitly if their
+ * orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrder()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrder() for details.
+ */
+ public ProtoFluentAssertion ignoringRepeatedFieldOrderOfFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return usingConfig(
+ config.ignoringRepeatedFieldOrderOfFieldDescriptors(asList(firstFieldDescriptor, rest)));
+ }
+
+ /**
+ * Specifies that the ordering of repeated fields for these explicitly specified field descriptors
+ * should be ignored when comparing for equality. Sub-fields must be specified explicitly if their
+ * orders are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringRepeatedFieldOrder()} instead to ignore order for all fields.
+ *
+ * @see #ignoringRepeatedFieldOrder() for details.
+ */
+ public ProtoFluentAssertion ignoringRepeatedFieldOrderOfFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(config.ignoringRepeatedFieldOrderOfFieldDescriptors(fieldDescriptors));
+ }
+
+ /**
+ * Specifies that, for all repeated and map fields, any elements in the 'actual' proto which are
+ * not found in the 'expected' proto are ignored, with the exception of fields in the expected
+ * proto which are empty. To ignore empty repeated fields as well, use {@link
+ * #comparingExpectedFieldsOnly}.
+ *
+ * <p>This rule is applied independently from {@link #ignoringRepeatedFieldOrder}. If ignoring
+ * repeated field order AND extra repeated field elements, all that is tested is that the expected
+ * elements comprise a subset of the actual elements. If not ignoring repeated field order, but
+ * still ignoring extra repeated field elements, the actual elements must contain a subsequence
+ * that matches the expected elements for the test to pass. (The subsequence rule does not apply
+ * to Map fields, which are always compared by key.)
+ */
+ public ProtoFluentAssertion ignoringExtraRepeatedFieldElements() {
+ return usingConfig(config.ignoringExtraRepeatedFieldElements());
+ }
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified top-level field
+ * numbers should be ignored. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if their extra elements are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElements()} instead to ignore these for all fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElements() for details.
+ */
+ public ProtoFluentAssertion ignoringExtraRepeatedFieldElementsOfFields(
+ int firstFieldNumber, int... rest) {
+ return usingConfig(
+ config.ignoringExtraRepeatedFieldElementsOfFields(asList(firstFieldNumber, rest)));
+ }
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified top-level field
+ * numbers should be ignored. Sub-fields must be specified explicitly (via {@link
+ * FieldDescriptor}) if their extra elements are to be ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElements()} instead to ignore these for all fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElements() for details.
+ */
+ public ProtoFluentAssertion ignoringExtraRepeatedFieldElementsOfFields(
+ Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.ignoringExtraRepeatedFieldElementsOfFields(fieldNumbers));
+ }
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified field descriptors
+ * should be ignored. Sub-fields must be specified explicitly if their extra elements are to be
+ * ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElements()} instead to ignore these for all fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElements() for details.
+ */
+ public ProtoFluentAssertion ignoringExtraRepeatedFieldElementsOfFieldDescriptors(
+ FieldDescriptor first, FieldDescriptor... rest) {
+ return usingConfig(
+ config.ignoringExtraRepeatedFieldElementsOfFieldDescriptors(asList(first, rest)));
+ }
+
+ /**
+ * Specifies that extra repeated field elements for these explicitly specified field descriptors
+ * should be ignored. Sub-fields must be specified explicitly if their extra elements are to be
+ * ignored as well.
+ *
+ * <p>Use {@link #ignoringExtraRepeatedFieldElements()} instead to ignore these for all fields.
+ *
+ * @see #ignoringExtraRepeatedFieldElements() for details.
+ */
+ public ProtoFluentAssertion ignoringExtraRepeatedFieldElementsOfFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(
+ config.ignoringExtraRepeatedFieldElementsOfFieldDescriptors(fieldDescriptors));
+ }
+
+ /**
+ * Compares double fields as equal if they are both finite and their absolute difference is less
+ * than or equal to {@code tolerance}.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public ProtoFluentAssertion usingDoubleTolerance(double tolerance) {
+ return usingConfig(config.usingDoubleTolerance(tolerance));
+ }
+
+ /**
+ * Compares double fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public ProtoFluentAssertion usingDoubleToleranceForFields(
+ double tolerance, int firstFieldNumber, int... rest) {
+ return usingConfig(
+ config.usingDoubleToleranceForFields(tolerance, asList(firstFieldNumber, rest)));
+ }
+
+ /**
+ * Compares double fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public ProtoFluentAssertion usingDoubleToleranceForFields(
+ double tolerance, Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.usingDoubleToleranceForFields(tolerance, fieldNumbers));
+ }
+
+ /**
+ * Compares double fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public ProtoFluentAssertion usingDoubleToleranceForFieldDescriptors(
+ double tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return usingConfig(
+ config.usingDoubleToleranceForFieldDescriptors(
+ tolerance, asList(firstFieldDescriptor, rest)));
+ }
+
+ /**
+ * Compares double fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public ProtoFluentAssertion usingDoubleToleranceForFieldDescriptors(
+ double tolerance, Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(config.usingDoubleToleranceForFieldDescriptors(tolerance, fieldDescriptors));
+ }
+
+ /**
+ * Compares float fields as equal if they are both finite and their absolute difference is less
+ * than or equal to {@code tolerance}.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public ProtoFluentAssertion usingFloatTolerance(float tolerance) {
+ return usingConfig(config.usingFloatTolerance(tolerance));
+ }
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public ProtoFluentAssertion usingFloatToleranceForFields(
+ float tolerance, int firstFieldNumber, int... rest) {
+ return usingConfig(
+ config.usingFloatToleranceForFields(tolerance, asList(firstFieldNumber, rest)));
+ }
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public ProtoFluentAssertion usingFloatToleranceForFields(
+ float tolerance, Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.usingFloatToleranceForFields(tolerance, fieldNumbers));
+ }
+
+ /**
+ * Compares float fields with these explicitly specified fields using the provided absolute
+ * tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public ProtoFluentAssertion usingFloatToleranceForFieldDescriptors(
+ float tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return usingConfig(
+ config.usingFloatToleranceForFieldDescriptors(
+ tolerance, asList(firstFieldDescriptor, rest)));
+ }
+
+ /**
+ * Compares float fields with these explicitly specified top-level field numbers using the
+ * provided absolute tolerance.
+ *
+ * @param tolerance A finite, non-negative tolerance.
+ */
+ public ProtoFluentAssertion usingFloatToleranceForFieldDescriptors(
+ float tolerance, Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(config.usingFloatToleranceForFieldDescriptors(tolerance, fieldDescriptors));
+ }
+
+ /**
+ * Limits the comparison of Protocol buffers to the fields set in the expected proto(s). When
+ * multiple protos are specified, the comparison is limited to the union of set fields in all the
+ * expected protos.
+ *
+ * <p>The "expected proto(s)" are those passed to the void method at the end of the {@code
+ * ProtoFluentAssertion} call-chain: For example, {@link #isEqualTo(Message)}, or {@link
+ * #isNotEqualTo(Message)}.
+ *
+ * <p>Fields not set in the expected proto(s) are ignored. In particular, proto3 fields which have
+ * their default values are ignored, as these are indistinguishable from unset fields. If you want
+ * to assert that a proto3 message has certain fields with default values, you cannot use this
+ * method.
+ */
+ public ProtoFluentAssertion comparingExpectedFieldsOnly() {
+ return usingConfig(config.comparingExpectedFieldsOnly());
+ }
+
+ /**
+ * Limits the comparison of Protocol buffers to the defined {@link FieldScope}.
+ *
+ * <p>This method is additive and has well-defined ordering semantics. If the invoking {@link
+ * ProtoFluentAssertion} is already scoped to a {@link FieldScope} {@code X}, and this method is
+ * invoked with {@link FieldScope} {@code Y}, the resultant {@link ProtoFluentAssertion} is
+ * constrained to the intersection of {@link FieldScope}s {@code X} and {@code Y}.
+ *
+ * <p>By default, {@link ProtoFluentAssertion} is constrained to {@link FieldScopes#all()}, that
+ * is, no fields are excluded from comparison.
+ */
+ public ProtoFluentAssertion withPartialScope(FieldScope fieldScope) {
+ return usingConfig(config.withPartialScope(checkNotNull(fieldScope, "fieldScope")));
+ }
+
+ /**
+ * Excludes the top-level message fields with the given tag numbers from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * numbers are ignored, and all sub-messages of type {@code M} will also have these field numbers
+ * ignored.
+ *
+ * <p>If an invalid field number is supplied, the terminal comparison operation will throw a
+ * runtime exception.
+ */
+ public ProtoFluentAssertion ignoringFields(int firstFieldNumber, int... rest) {
+ return ignoringFields(asList(firstFieldNumber, rest));
+ }
+
+ /**
+ * Excludes the top-level message fields with the given tag numbers from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * numbers are ignored, and all sub-messages of type {@code M} will also have these field numbers
+ * ignored.
+ *
+ * <p>If an invalid field number is supplied, the terminal comparison operation will throw a
+ * runtime exception.
+ */
+ public ProtoFluentAssertion ignoringFields(Iterable<Integer> fieldNumbers) {
+ return usingConfig(config.ignoringFields(fieldNumbers));
+ }
+
+ /**
+ * Excludes all message fields matching the given {@link FieldDescriptor}s from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * descriptors are ignored, no matter where they occur in the tree.
+ *
+ * <p>If a field descriptor which does not, or cannot occur in the proto structure is supplied, it
+ * is silently ignored.
+ */
+ public ProtoFluentAssertion ignoringFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return ignoringFieldDescriptors(asList(firstFieldDescriptor, rest));
+ }
+
+ /**
+ * Excludes all message fields matching the given {@link FieldDescriptor}s from the comparison.
+ *
+ * <p>This method adds on any previous {@link FieldScope} related settings, overriding previous
+ * changes to ensure the specified fields are ignored recursively. All sub-fields of these field
+ * descriptors are ignored, no matter where they occur in the tree.
+ *
+ * <p>If a field descriptor which does not, or cannot occur in the proto structure is supplied, it
+ * is silently ignored.
+ */
+ public ProtoFluentAssertion ignoringFieldDescriptors(Iterable<FieldDescriptor> fieldDescriptors) {
+ return usingConfig(config.ignoringFieldDescriptors(fieldDescriptors));
+ }
+
+ /**
+ * Excludes all specific field paths under the argument {@link FieldScope} from the comparison.
+ *
+ * <p>This method is additive and has well-defined ordering semantics. If the invoking {@link
+ * ProtoFluentAssertion} is already scoped to a {@link FieldScope} {@code X}, and this method is
+ * invoked with {@link FieldScope} {@code Y}, the resultant {@link ProtoFluentAssertion} is
+ * constrained to the subtraction of {@code X - Y}.
+ *
+ * <p>By default, {@link ProtoFluentAssertion} is constrained to {@link FieldScopes#all()}, that
+ * is, no fields are excluded from comparison.
+ */
+ public ProtoFluentAssertion ignoringFieldScope(FieldScope fieldScope) {
+ return usingConfig(config.ignoringFieldScope(checkNotNull(fieldScope, "fieldScope")));
+ }
+
+ /**
+ * If set, in the event of a comparison failure, the error message printed will list only those
+ * specific fields that did not match between the actual and expected values. Useful for very
+ * large protocol buffers.
+ *
+ * <p>This a purely cosmetic setting, and it has no effect on the behavior of the test.
+ */
+ public ProtoFluentAssertion reportingMismatchesOnly() {
+ return usingConfig(config.reportingMismatchesOnly());
+ }
+
+ /**
+ * Specifies the {@link TypeRegistry} and {@link ExtensionRegistry} to use for {@link
+ * com.google.protobuf.Any Any} messages.
+ *
+ * <p>To compare the value of an {@code Any} message, ProtoTruth looks in the given type registry
+ * for a descriptor for the message's type URL:
+ *
+ * <ul>
+ * <li>If ProtoTruth finds a descriptor, it unpacks the value and compares it against the
+ * expected value, respecting any configuration methods used for the assertion.
+ * <li>If ProtoTruth does not find a descriptor (or if the value can't be deserialized with the
+ * descriptor), it compares the raw, serialized bytes of the expected and actual values.
+ * </ul>
+ *
+ * <p>When ProtoTruth unpacks a value, it is parsing a serialized proto. That proto may contain
+ * extensions. To look up those extensions, ProtoTruth uses the provided {@link
+ * ExtensionRegistry}.
+ *
+ * @since 1.1
+ */
+ public ProtoFluentAssertion unpackingAnyUsing(
+ TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry) {
+ return usingConfig(config.unpackingAnyUsing(typeRegistry, extensionRegistry));
+ }
+
+ private static boolean sameClassMessagesWithDifferentDescriptors(
+ @Nullable Message actual, @Nullable Object expected) {
+ if (actual == null
+ || !(expected instanceof Message)
+ || actual.getClass() != expected.getClass()) {
+ return false;
+ }
+
+ return actual.getDescriptorForType() != ((Message) expected).getDescriptorForType();
+ }
+
+ private static boolean notMessagesWithSameDescriptor(
+ @Nullable Message actual, @Nullable Object expected) {
+ if (actual != null && expected instanceof Message) {
+ return actual.getDescriptorForType() != ((Message) expected).getDescriptorForType();
+ }
+ return true;
+ }
+
+ @Override
+ public void isEqualTo(@Nullable Object expected) {
+ if (sameClassMessagesWithDifferentDescriptors(actual, expected)) {
+ // This can happen with DynamicMessages, and it's very confusing if they both have the
+ // same string.
+ failWithoutActual(
+ simpleFact("Not true that messages compare equal; they have different descriptors."),
+ fact("expected", expected),
+ fact("with descriptor", ((Message) expected).getDescriptorForType()),
+ fact("but was", actual),
+ fact("with descriptor", actual.getDescriptorForType()));
+ } else if (notMessagesWithSameDescriptor(actual, expected)) {
+ super.isEqualTo(expected);
+ } else {
+ DiffResult diffResult =
+ makeDifferencer((Message) expected).diffMessages(actual, (Message) expected);
+ if (!diffResult.isMatched()) {
+ failWithoutActual(
+ simpleFact(
+ "Not true that messages compare equal.\n"
+ + diffResult.printToString(config.reportMismatchesOnly())));
+ }
+ }
+ }
+
+ @Override
+ public void isNotEqualTo(@Nullable Object expected) {
+ if (notMessagesWithSameDescriptor(actual, expected)) {
+ super.isNotEqualTo(expected);
+ } else {
+ DiffResult diffResult =
+ makeDifferencer((Message) expected).diffMessages(actual, (Message) expected);
+ if (diffResult.isMatched()) {
+ failWithoutActual(
+ simpleFact(
+ "Not true that messages compare not equal.\n"
+ + diffResult.printToString(config.reportMismatchesOnly())));
+ }
+ }
+ }
+
+ @Override
+ public void hasAllRequiredFields() {
+ if (!actual.isInitialized()) {
+ failWithoutActual(
+ simpleFact("expected to have all required fields set"),
+ fact("but was missing", actual.findInitializationErrors()),
+ fact("proto was", actualCustomStringRepresentationForProtoPackageMembersToCall()));
+ }
+ }
+
+ private ProtoTruthMessageDifferencer makeDifferencer(Message expected) {
+ return config
+ .withExpectedMessages(Arrays.asList(expected))
+ .toMessageDifferencer(actual.getDescriptorForType());
+ }
+
+ static final class ProtoFluentAssertionImpl implements ProtoFluentAssertion {
+ private final ProtoSubject protoSubject;
+
+ ProtoFluentAssertionImpl(ProtoSubject protoSubject) {
+ this.protoSubject = protoSubject;
+ }
+
+ @Override
+ public ProtoFluentAssertion ignoringFieldAbsence() {
+ return protoSubject.ignoringFieldAbsence();
+ }
+
+ @Override
+ public ProtoFluentAssertion ignoringFieldAbsenceOfFields(int firstFieldNumber, int... rest) {
+ return protoSubject.ignoringFieldAbsenceOfFields(firstFieldNumber, rest);
+ }
+
+ @Override
+ public ProtoFluentAssertion ignoringFieldAbsenceOfFields(Iterable<Integer> fieldNumbers) {
+ return protoSubject.ignoringFieldAbsenceOfFields(fieldNumbers);
+ }
+
+ @Override
+ public ProtoFluentAssertion ignoringFieldAbsenceOfFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return protoSubject.ignoringFieldAbsenceOfFieldDescriptors(firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public ProtoFluentAssertion ignoringFieldAbsenceOfFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return protoSubject.ignoringFieldAbsenceOfFieldDescriptors(fieldDescriptors);
+ }
+
+ @Override
+ public ProtoFluentAssertion ignoringRepeatedFieldOrder() {
+ return protoSubject.ignoringRepeatedFieldOrder();
+ }
+
+ @Override
+ public ProtoFluentAssertion ignoringRepeatedFieldOrderOfFields(
+ int firstFieldNumber, int... rest) {
+ return protoSubject.ignoringRepeatedFieldOrderOfFields(firstFieldNumber, rest);
+ }
+
+ @Override
+ public ProtoFluentAssertion ignoringRepeatedFieldOrderOfFields(Iterable<Integer> fieldNumbers) {
+ return protoSubject.ignoringRepeatedFieldOrderOfFields(fieldNumbers);
+ }
+
+ @Override
+ public ProtoFluentAssertion ignoringRepeatedFieldOrderOfFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return protoSubject.ignoringRepeatedFieldOrderOfFieldDescriptors(firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public ProtoFluentAssertion ignoringRepeatedFieldOrderOfFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return protoSubject.ignoringRepeatedFieldOrderOfFieldDescriptors(fieldDescriptors);
+ }
+
+ @Override
+ public ProtoFluentAssertion ignoringExtraRepeatedFieldElements() {
+ return protoSubject.ignoringExtraRepeatedFieldElements();
+ }
+
+ @Override
+ public ProtoFluentAssertion ignoringExtraRepeatedFieldElementsOfFields(
+ int firstFieldNumber, int... rest) {
+ return protoSubject.ignoringExtraRepeatedFieldElementsOfFields(firstFieldNumber, rest);
+ }
+
+ @Override
+ public ProtoFluentAssertion ignoringExtraRepeatedFieldElementsOfFields(
+ Iterable<Integer> fieldNumbers) {
+ return protoSubject.ignoringExtraRepeatedFieldElementsOfFields(fieldNumbers);
+ }
+
+ @Override
+ public ProtoFluentAssertion ignoringExtraRepeatedFieldElementsOfFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return protoSubject.ignoringExtraRepeatedFieldElementsOfFieldDescriptors(
+ firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public ProtoFluentAssertion ignoringExtraRepeatedFieldElementsOfFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return protoSubject.ignoringExtraRepeatedFieldElementsOfFieldDescriptors(fieldDescriptors);
+ }
+
+ @Override
+ public ProtoFluentAssertion usingDoubleTolerance(double tolerance) {
+ return protoSubject.usingDoubleTolerance(tolerance);
+ }
+
+ @Override
+ public ProtoFluentAssertion usingDoubleToleranceForFields(
+ double tolerance, int firstFieldNumber, int... rest) {
+ return protoSubject.usingDoubleToleranceForFields(tolerance, firstFieldNumber, rest);
+ }
+
+ @Override
+ public ProtoFluentAssertion usingDoubleToleranceForFields(
+ double tolerance, Iterable<Integer> fieldNumbers) {
+ return protoSubject.usingDoubleToleranceForFields(tolerance, fieldNumbers);
+ }
+
+ @Override
+ public ProtoFluentAssertion usingDoubleToleranceForFieldDescriptors(
+ double tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return protoSubject.usingDoubleToleranceForFieldDescriptors(
+ tolerance, firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public ProtoFluentAssertion usingDoubleToleranceForFieldDescriptors(
+ double tolerance, Iterable<FieldDescriptor> fieldDescriptors) {
+ return protoSubject.usingDoubleToleranceForFieldDescriptors(tolerance, fieldDescriptors);
+ }
+
+ @Override
+ public ProtoFluentAssertion usingFloatTolerance(float tolerance) {
+ return protoSubject.usingFloatTolerance(tolerance);
+ }
+
+ @Override
+ public ProtoFluentAssertion usingFloatToleranceForFields(
+ float tolerance, int firstFieldNumber, int... rest) {
+ return protoSubject.usingFloatToleranceForFields(tolerance, firstFieldNumber, rest);
+ }
+
+ @Override
+ public ProtoFluentAssertion usingFloatToleranceForFields(
+ float tolerance, Iterable<Integer> fieldNumbers) {
+ return protoSubject.usingFloatToleranceForFields(tolerance, fieldNumbers);
+ }
+
+ @Override
+ public ProtoFluentAssertion usingFloatToleranceForFieldDescriptors(
+ float tolerance, FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return protoSubject.usingFloatToleranceForFieldDescriptors(
+ tolerance, firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public ProtoFluentAssertion usingFloatToleranceForFieldDescriptors(
+ float tolerance, Iterable<FieldDescriptor> fieldDescriptors) {
+ return protoSubject.usingFloatToleranceForFieldDescriptors(tolerance, fieldDescriptors);
+ }
+
+ @Override
+ public ProtoFluentAssertion comparingExpectedFieldsOnly() {
+ return protoSubject.comparingExpectedFieldsOnly();
+ }
+
+ @Override
+ public ProtoFluentAssertion withPartialScope(FieldScope fieldScope) {
+ return protoSubject.withPartialScope(fieldScope);
+ }
+
+ @Override
+ public ProtoFluentAssertion ignoringFields(int firstFieldNumber, int... rest) {
+ return protoSubject.ignoringFields(firstFieldNumber, rest);
+ }
+
+ @Override
+ public ProtoFluentAssertion ignoringFields(Iterable<Integer> fieldNumbers) {
+ return protoSubject.ignoringFields(fieldNumbers);
+ }
+
+ @Override
+ public ProtoFluentAssertion ignoringFieldDescriptors(
+ FieldDescriptor firstFieldDescriptor, FieldDescriptor... rest) {
+ return protoSubject.ignoringFieldDescriptors(firstFieldDescriptor, rest);
+ }
+
+ @Override
+ public ProtoFluentAssertion ignoringFieldDescriptors(
+ Iterable<FieldDescriptor> fieldDescriptors) {
+ return protoSubject.ignoringFieldDescriptors(fieldDescriptors);
+ }
+
+ @Override
+ public ProtoFluentAssertion ignoringFieldScope(FieldScope fieldScope) {
+ return protoSubject.ignoringFieldScope(fieldScope);
+ }
+
+ @Override
+ public ProtoFluentAssertion reportingMismatchesOnly() {
+ return protoSubject.reportingMismatchesOnly();
+ }
+
+ @Override
+ public ProtoFluentAssertion unpackingAnyUsing(
+ TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry) {
+ return protoSubject.unpackingAnyUsing(typeRegistry, extensionRegistry);
+ }
+
+ @Override
+ public void isEqualTo(@Nullable Message expected) {
+ protoSubject.isEqualTo(expected);
+ }
+
+ @Override
+ public void isNotEqualTo(@Nullable Message expected) {
+ protoSubject.isNotEqualTo(expected);
+ }
+
+ /**
+ * Same as {@link #isEqualTo(Message)}, except it returns true on success and false on failure
+ * without throwing any exceptions.
+ */
+ boolean testIsEqualTo(@Nullable Message expected) {
+ if (notMessagesWithSameDescriptor(protoSubject.actual, expected)) {
+ return Objects.equal(protoSubject.actual, expected);
+ } else {
+ return protoSubject
+ .makeDifferencer(expected)
+ .diffMessages(protoSubject.actual, expected)
+ .isMatched();
+ }
+ }
+ }
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoSubjectBuilder.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoSubjectBuilder.java
new file mode 100644
index 00000000..0d813aa2
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoSubjectBuilder.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import com.google.common.collect.Multimap;
+import com.google.common.truth.CustomSubjectBuilder;
+import com.google.common.truth.FailureMetadata;
+import com.google.protobuf.Message;
+import com.google.protobuf.MessageLite;
+import java.util.Map;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * {@link CustomSubjectBuilder} which aggregates all Proto-related {@link
+ * com.google.common.truth.Subject} classes into a single place.
+ *
+ * <p>To obtain an instance, call {@code assertAbout(ProtoTruth.protos())}.
+ */
+public final class ProtoSubjectBuilder extends CustomSubjectBuilder {
+
+ /** Factory for ProtoSubjectBuilder. */
+ private static class Factory implements CustomSubjectBuilder.Factory<ProtoSubjectBuilder> {
+ private static final Factory INSTANCE = new Factory();
+
+ @Override
+ public ProtoSubjectBuilder createSubjectBuilder(FailureMetadata failureMetadata) {
+ return new ProtoSubjectBuilder(failureMetadata);
+ }
+ }
+
+ static CustomSubjectBuilder.Factory<ProtoSubjectBuilder> factory() {
+ return Factory.INSTANCE;
+ }
+
+ private ProtoSubjectBuilder(FailureMetadata failureMetadata) {
+ super(failureMetadata);
+ }
+
+ public LiteProtoSubject that(@Nullable MessageLite messageLite) {
+ return new LiteProtoSubject(metadata(), messageLite);
+ }
+
+ public ProtoSubject that(@Nullable Message message) {
+ return new ProtoSubject(metadata(), message);
+ }
+
+ public <M extends Message> IterableOfProtosSubject<M> that(@Nullable Iterable<M> messages) {
+ return new IterableOfProtosSubject<M>(metadata(), messages);
+ }
+
+ public <M extends Message> MapWithProtoValuesSubject<M> that(@Nullable Map<?, M> map) {
+ return new MapWithProtoValuesSubject<>(metadata(), map);
+ }
+
+ public <M extends Message> MultimapWithProtoValuesSubject<M> that(@Nullable Multimap<?, M> map) {
+ return new MultimapWithProtoValuesSubject<>(metadata(), map);
+ }
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoTruth.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoTruth.java
new file mode 100644
index 00000000..fbdaa9e0
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoTruth.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import static com.google.common.truth.Truth.assertAbout;
+
+import com.google.common.collect.Multimap;
+import com.google.common.truth.CustomSubjectBuilder;
+import com.google.common.truth.IterableSubject;
+import com.google.common.truth.MapSubject;
+import com.google.common.truth.MultimapSubject;
+import com.google.common.truth.StandardSubjectBuilder;
+import com.google.protobuf.Message;
+import com.google.protobuf.MessageLite;
+import java.util.Map;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A set of static methods to begin a Truth assertion chain for protocol buffers.
+ *
+ * <p>Note: Usage of different failure strategies such as <em>assume</em> and <em>expect</em> should
+ * rely on {@link StandardSubjectBuilder#about(CustomSubjectBuilder.Factory)} to begin a chain with
+ * those alternative behaviors.
+ */
+public final class ProtoTruth {
+
+ /**
+ * Returns a {@link CustomSubjectBuilder.Factory}, akin to a {@link
+ * com.google.common.truth.Subject.Factory}, which can be used to assert on multiple types of
+ * Protos and collections containing them.
+ */
+ public static CustomSubjectBuilder.Factory<ProtoSubjectBuilder> protos() {
+ return ProtoSubjectBuilder.factory();
+ }
+
+ /** Assert on a single {@link MessageLite} instance. */
+ public static LiteProtoSubject assertThat(@Nullable MessageLite messageLite) {
+ return assertAbout(protos()).that(messageLite);
+ }
+
+ /** Assert on a single {@link Message} instance. */
+ public static ProtoSubject assertThat(@Nullable Message message) {
+ return assertAbout(protos()).that(message);
+ }
+
+ /**
+ * Assert on a sequence of {@link Message}s.
+ *
+ * <p>This allows for the equality configurations on {@link ProtoSubject} to be applied to all
+ * comparison tests available on {@link IterableSubject.UsingCorrespondence}.
+ */
+ // Note: We must specify M explicitly here. The presence of the type parameter makes this method
+ // signature distinct from Truth.assertThat(Iterable<?>), and allows users to import both static
+ // methods without conflict. If this method instead accepted Iterable<? extends Message>, this
+ // would result in method ambiguity errors.
+ // See http://stackoverflow.com/a/8467804 for a more thorough explanation.
+ public static <M extends Message> IterableOfProtosSubject<M> assertThat(
+ @Nullable Iterable<M> messages) {
+ return assertAbout(protos()).that(messages);
+ }
+
+ /**
+ * Assert on a map with {@link Message} values.
+ *
+ * <p>This allows for the equality configurations on {@link ProtoSubject} to be applied to all
+ * comparison tests available on {@link MapSubject.UsingCorrespondence}.
+ */
+ public static <M extends Message> MapWithProtoValuesSubject<M> assertThat(
+ @Nullable Map<?, M> map) {
+ return assertAbout(protos()).that(map);
+ }
+
+ /**
+ * Assert on a {@link Multimap} with {@link Message} values.
+ *
+ * <p>This allows for the equality configurations on {@link ProtoSubject} to be applied to all
+ * comparison tests available on {@link MultimapSubject.UsingCorrespondence}.
+ */
+ public static <M extends Message> MultimapWithProtoValuesSubject<M> assertThat(
+ @Nullable Multimap<?, M> multimap) {
+ return assertAbout(protos()).that(multimap);
+ }
+
+ private ProtoTruth() {}
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoTruthMessageDifferencer.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoTruthMessageDifferencer.java
new file mode 100644
index 00000000..d843a61d
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoTruthMessageDifferencer.java
@@ -0,0 +1,971 @@
+/*
+ * Copyright (c) 2017 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.truth.Correspondence;
+import com.google.common.truth.extensions.proto.DiffResult.RepeatedField;
+import com.google.common.truth.extensions.proto.DiffResult.SingularField;
+import com.google.common.truth.extensions.proto.DiffResult.UnknownFieldSetDiff;
+import com.google.common.truth.extensions.proto.RecursableDiffEntity.WithResultCode.Result;
+import com.google.protobuf.Any;
+import com.google.protobuf.Descriptors.Descriptor;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.Descriptors.FieldDescriptor.JavaType;
+import com.google.protobuf.Descriptors.FileDescriptor.Syntax;
+import com.google.protobuf.Message;
+import com.google.protobuf.TextFormat;
+import com.google.protobuf.UnknownFieldSet;
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Tool to differentiate two messages with the same {@link Descriptor}, subject to the rules set out
+ * in a {@link FluentEqualityConfig}.
+ *
+ * <p>A {@code ProtoTruthMessageDifferencer} is immutable and thread-safe. Its outputs, however,
+ * have caching behaviors and are not thread-safe.
+ */
+final class ProtoTruthMessageDifferencer {
+ private final FluentEqualityConfig rootConfig;
+ private final Descriptor rootDescriptor;
+
+ private ProtoTruthMessageDifferencer(FluentEqualityConfig rootConfig, Descriptor descriptor) {
+ rootConfig.validate(descriptor, FieldDescriptorValidator.ALLOW_ALL);
+
+ this.rootConfig = rootConfig;
+ this.rootDescriptor = descriptor;
+ }
+
+ /** Create a new {@link ProtoTruthMessageDifferencer} for the given config and descriptor. */
+ static ProtoTruthMessageDifferencer create(
+ FluentEqualityConfig rootConfig, Descriptor descriptor) {
+ return new ProtoTruthMessageDifferencer(rootConfig, descriptor);
+ }
+
+ /** Compare the two non-null messages, and return a detailed comparison report. */
+ DiffResult diffMessages(Message actual, Message expected) {
+ checkNotNull(actual);
+ checkNotNull(expected);
+ checkArgument(
+ actual.getDescriptorForType() == expected.getDescriptorForType(),
+ "The actual [%s] and expected [%s] message descriptors do not match.",
+ actual.getDescriptorForType(),
+ expected.getDescriptorForType());
+
+ return diffMessages(actual, expected, rootConfig);
+ }
+
+ private DiffResult diffMessages(Message actual, Message expected, FluentEqualityConfig config) {
+ if (actual.getDescriptorForType().equals(Any.getDescriptor())) {
+ return diffAnyMessages(actual, expected, config);
+ }
+ DiffResult.Builder builder = DiffResult.newBuilder().setActual(actual).setExpected(expected);
+
+ // Compare known fields.
+ Map<FieldDescriptor, Object> actualFields = actual.getAllFields();
+ Map<FieldDescriptor, Object> expectedFields = expected.getAllFields();
+ for (FieldDescriptor fieldDescriptor :
+ Sets.union(actualFields.keySet(), expectedFields.keySet())) {
+ // Check if we should ignore this field. If the result is nonrecursive, proceed anyway, but
+ // the field will be considered ignored in the final diff report if no sub-fields get compared
+ // (i.e., the sub-DiffResult winds up empty). This allows us support FieldScopeLogic
+ // disjunctions without repeating recursive work.
+ SubScopeId subScopeId = SubScopeId.of(fieldDescriptor);
+ FieldScopeResult shouldCompare =
+ config.compareFieldsScope().policyFor(rootDescriptor, subScopeId);
+ if (shouldCompare == FieldScopeResult.EXCLUDED_RECURSIVELY) {
+ builder.addSingularField(
+ fieldDescriptor.getNumber(), SingularField.ignored(name(fieldDescriptor)));
+ continue;
+ }
+
+ if (fieldDescriptor.isRepeated()) {
+ if (fieldDescriptor.isMapField()) {
+ Map<Object, Object> actualMap = toProtoMap(actualFields.get(fieldDescriptor));
+ Map<Object, Object> expectedMap = toProtoMap(expectedFields.get(fieldDescriptor));
+
+ ImmutableSet<Object> actualAndExpectedKeys =
+ Sets.union(actualMap.keySet(), expectedMap.keySet()).immutableCopy();
+ builder.addAllSingularFields(
+ fieldDescriptor.getNumber(),
+ compareMapFieldsByKey(
+ actualMap,
+ expectedMap,
+ actualAndExpectedKeys,
+ fieldDescriptor,
+ config.subScope(rootDescriptor, subScopeId)));
+ } else {
+ List<?> actualList = toProtoList(actualFields.get(fieldDescriptor));
+ List<?> expectedList = toProtoList(expectedFields.get(fieldDescriptor));
+
+ boolean ignoreRepeatedFieldOrder =
+ config.ignoreRepeatedFieldOrderScope().contains(rootDescriptor, subScopeId);
+ boolean ignoreExtraRepeatedFieldElements =
+ config.ignoreExtraRepeatedFieldElementsScope().contains(rootDescriptor, subScopeId);
+ if (ignoreRepeatedFieldOrder) {
+ builder.addRepeatedField(
+ fieldDescriptor.getNumber(),
+ compareRepeatedFieldIgnoringOrder(
+ actualList,
+ expectedList,
+ shouldCompare == FieldScopeResult.EXCLUDED_NONRECURSIVELY,
+ fieldDescriptor,
+ ignoreExtraRepeatedFieldElements,
+ config.subScope(rootDescriptor, subScopeId)));
+ } else if (ignoreExtraRepeatedFieldElements && !expectedList.isEmpty()) {
+ builder.addRepeatedField(
+ fieldDescriptor.getNumber(),
+ compareRepeatedFieldExpectingSubsequence(
+ actualList,
+ expectedList,
+ shouldCompare == FieldScopeResult.EXCLUDED_NONRECURSIVELY,
+ fieldDescriptor,
+ config.subScope(rootDescriptor, subScopeId)));
+ } else {
+ builder.addAllSingularFields(
+ fieldDescriptor.getNumber(),
+ compareRepeatedFieldByIndices(
+ actualList,
+ expectedList,
+ shouldCompare == FieldScopeResult.EXCLUDED_NONRECURSIVELY,
+ fieldDescriptor,
+ config.subScope(rootDescriptor, subScopeId)));
+ }
+ }
+ } else {
+ builder.addSingularField(
+ fieldDescriptor.getNumber(),
+ compareSingularValue(
+ actualFields.get(fieldDescriptor),
+ expectedFields.get(fieldDescriptor),
+ actual.getDefaultInstanceForType().getField(fieldDescriptor),
+ shouldCompare == FieldScopeResult.EXCLUDED_NONRECURSIVELY,
+ fieldDescriptor,
+ name(fieldDescriptor),
+ config.subScope(rootDescriptor, subScopeId)));
+ }
+ }
+
+ // Compare unknown fields.
+ if (!config.ignoreFieldAbsenceScope().isAll()) {
+ UnknownFieldSetDiff diff =
+ diffUnknowns(actual.getUnknownFields(), expected.getUnknownFields(), config);
+ builder.setUnknownFields(diff);
+ }
+
+ return builder.build();
+ }
+
+ private DiffResult diffAnyMessages(
+ Message actual, Message expected, FluentEqualityConfig config) {
+ DiffResult.Builder builder = DiffResult.newBuilder().setActual(actual).setExpected(expected);
+
+ // Compare the TypeUrl fields.
+ FieldScopeResult shouldCompareTypeUrl =
+ config.compareFieldsScope().policyFor(rootDescriptor, AnyUtils.typeUrlSubScopeId());
+ SingularField typeUrlDiffResult;
+ if (!shouldCompareTypeUrl.included()) {
+ typeUrlDiffResult = SingularField.ignored(name(AnyUtils.typeUrlFieldDescriptor()));
+ } else {
+ typeUrlDiffResult =
+ compareSingularPrimitive(
+ actual.getField(AnyUtils.typeUrlFieldDescriptor()),
+ expected.getField(AnyUtils.typeUrlFieldDescriptor()),
+ /* defaultValue= */ "",
+ AnyUtils.typeUrlFieldDescriptor(),
+ name(AnyUtils.typeUrlFieldDescriptor()),
+ config.subScope(rootDescriptor, AnyUtils.typeUrlSubScopeId()));
+ }
+ builder.addSingularField(Any.TYPE_URL_FIELD_NUMBER, typeUrlDiffResult);
+
+ // Try to unpack the value fields using the TypeRegister and url from the type_url field. If
+ // that does not work then we revert to the original behaviour compare the bytes strings.
+ FieldScopeResult shouldCompareValue =
+ config.compareFieldsScope().policyFor(rootDescriptor, AnyUtils.valueSubScopeId());
+ SingularField valueDiffResult;
+ if (shouldCompareValue == FieldScopeResult.EXCLUDED_RECURSIVELY) {
+ valueDiffResult = SingularField.ignored(name(AnyUtils.valueFieldDescriptor()));
+ } else {
+ Optional<Message> unpackedActual = AnyUtils.unpack(actual, config);
+ Optional<Message> unpackedExpected = AnyUtils.unpack(expected, config);
+ if (unpackedActual.isPresent()
+ && unpackedExpected.isPresent()
+ && descriptorsMatch(unpackedActual.get(), unpackedExpected.get())) {
+ Message defaultMessage = unpackedActual.get().getDefaultInstanceForType();
+ valueDiffResult =
+ compareSingularMessage(
+ unpackedActual.get(),
+ unpackedExpected.get(),
+ defaultMessage,
+ shouldCompareValue == FieldScopeResult.EXCLUDED_NONRECURSIVELY,
+ AnyUtils.valueFieldDescriptor(),
+ name(AnyUtils.valueFieldDescriptor()),
+ config.subScope(rootDescriptor, AnyUtils.valueSubScopeId()));
+ } else {
+ valueDiffResult =
+ compareSingularValue(
+ actual.getField(AnyUtils.valueFieldDescriptor()),
+ expected.getField(AnyUtils.valueFieldDescriptor()),
+ AnyUtils.valueFieldDescriptor().getDefaultValue(),
+ shouldCompareValue == FieldScopeResult.EXCLUDED_NONRECURSIVELY,
+ AnyUtils.valueFieldDescriptor(),
+ name(AnyUtils.valueFieldDescriptor()),
+ config.subScope(rootDescriptor, AnyUtils.valueSubScopeId()));
+ }
+ }
+ builder.addSingularField(Any.VALUE_FIELD_NUMBER, valueDiffResult);
+
+ // Compare unknown fields.
+ if (!config.ignoreFieldAbsenceScope().isAll()) {
+ UnknownFieldSetDiff diff =
+ diffUnknowns(actual.getUnknownFields(), expected.getUnknownFields(), config);
+ builder.setUnknownFields(diff);
+ }
+
+ return builder.build();
+ }
+
+ private static boolean descriptorsMatch(Message actual, Message expected) {
+ return actual.getDescriptorForType().equals(expected.getDescriptorForType());
+ }
+
+ // Helper which takes a proto map in List<Message> form, and converts it to a Map<Object, Object>
+ // by extracting the keys and values from the generated map-entry submessages. Returns an empty
+ // map if null is passed in.
+ private static ImmutableMap<Object, Object> toProtoMap(@Nullable Object container) {
+ if (container == null) {
+ return ImmutableMap.of();
+ }
+ List<?> entryMessages = (List<?>) container;
+
+ // Can't use an ImmutableMap.Builder because proto wire format could have multiple entries with
+ // the same key. Documented behaviour is to use the last seen entry.
+ Map<Object, Object> retVal = Maps.newHashMap();
+ for (Object entry : entryMessages) {
+ Message message = (Message) entry;
+ retVal.put(valueAtFieldNumber(message, 1), valueAtFieldNumber(message, 2));
+ }
+ return ImmutableMap.copyOf(retVal);
+ }
+
+ private static Object valueAtFieldNumber(Message message, int fieldNumber) {
+ FieldDescriptor field = message.getDescriptorForType().findFieldByNumber(fieldNumber);
+ Object value = message.getAllFields().get(field);
+ return value != null ? value : field.getDefaultValue();
+ }
+
+ // Takes a List<Object> or null, and returns the casted list in the first case, an empty list in
+ // the latter case.
+ private static List<?> toProtoList(@Nullable Object container) {
+ if (container == null) {
+ return Collections.emptyList();
+ }
+ return (List<?>) container;
+ }
+
+ private List<SingularField> compareMapFieldsByKey(
+ Map<Object, Object> actualMap,
+ Map<Object, Object> expectedMap,
+ Set<Object> actualAndExpectedKeys,
+ FieldDescriptor mapFieldDescriptor,
+ FluentEqualityConfig mapConfig) {
+ FieldDescriptor keyFieldDescriptor = mapFieldDescriptor.getMessageType().findFieldByNumber(1);
+ FieldDescriptor valueFieldDescriptor = mapFieldDescriptor.getMessageType().findFieldByNumber(2);
+ SubScopeId valueSubScopeId = SubScopeId.of(valueFieldDescriptor);
+
+ // We never ignore the key, no matter what the logic dictates.
+ FieldScopeResult compareValues =
+ mapConfig.compareFieldsScope().policyFor(rootDescriptor, valueSubScopeId);
+ if (compareValues == FieldScopeResult.EXCLUDED_RECURSIVELY) {
+ return ImmutableList.of(SingularField.ignored(name(mapFieldDescriptor)));
+ }
+
+ boolean ignoreExtraRepeatedFieldElements =
+ mapConfig
+ .ignoreExtraRepeatedFieldElementsScope()
+ .contains(rootDescriptor, SubScopeId.of(mapFieldDescriptor));
+
+ FluentEqualityConfig valuesConfig = mapConfig.subScope(rootDescriptor, valueSubScopeId);
+
+ ImmutableList.Builder<SingularField> builder =
+ ImmutableList.builderWithExpectedSize(actualAndExpectedKeys.size());
+ for (Object key : actualAndExpectedKeys) {
+ @Nullable Object actualValue = actualMap.get(key);
+ @Nullable Object expectedValue = expectedMap.get(key);
+ if (ignoreExtraRepeatedFieldElements && !expectedMap.isEmpty() && expectedValue == null) {
+ builder.add(
+ SingularField.ignored(indexedName(mapFieldDescriptor, key, keyFieldDescriptor)));
+ } else {
+ builder.add(
+ compareSingularValue(
+ actualValue,
+ expectedValue,
+ /*defaultValue=*/ null,
+ compareValues == FieldScopeResult.EXCLUDED_NONRECURSIVELY,
+ valueFieldDescriptor,
+ indexedName(mapFieldDescriptor, key, keyFieldDescriptor),
+ valuesConfig));
+ }
+ }
+
+ return builder.build();
+ }
+
+ private RepeatedField compareRepeatedFieldIgnoringOrder(
+ List<?> actualList,
+ List<?> expectedList,
+ boolean excludeNonRecursive,
+ FieldDescriptor fieldDescriptor,
+ boolean ignoreExtraRepeatedFieldElements,
+ FluentEqualityConfig config) {
+ RepeatedField.Builder builder =
+ RepeatedField.newBuilder()
+ .setFieldDescriptor(fieldDescriptor)
+ .setActual(actualList)
+ .setExpected(expectedList);
+
+ // TODO(user): Use maximum bipartite matching here, instead of greedy matching.
+ Set<Integer> unmatchedActual = setForRange(actualList.size());
+ Set<Integer> unmatchedExpected = setForRange(expectedList.size());
+ for (int i = 0; i < actualList.size(); i++) {
+ Object actual = actualList.get(i);
+ for (int j : unmatchedExpected) {
+ Object expected = expectedList.get(j);
+ RepeatedField.PairResult pairResult =
+ compareRepeatedFieldElementPair(
+ actual, expected, excludeNonRecursive, fieldDescriptor, i, j, config);
+ if (pairResult.isMatched()) {
+ // Found a match - remove both these elements from the candidate pools.
+ builder.addPairResult(pairResult);
+ unmatchedActual.remove(i);
+ unmatchedExpected.remove(j);
+ break;
+ }
+ }
+ }
+
+ // Record remaining unmatched elements.
+ for (int i : unmatchedActual) {
+ if (ignoreExtraRepeatedFieldElements && !expectedList.isEmpty()) {
+ builder.addPairResult(
+ RepeatedField.PairResult.newBuilder()
+ .setResult(Result.IGNORED)
+ .setActual(actualList.get(i))
+ .setActualFieldIndex(i)
+ .setFieldDescriptor(fieldDescriptor)
+ .build());
+ } else {
+ builder.addPairResult(
+ compareRepeatedFieldElementPair(
+ actualList.get(i),
+ /*expected=*/ null,
+ excludeNonRecursive,
+ fieldDescriptor,
+ i,
+ /*expectedFieldIndex=*/ null,
+ config));
+ }
+ }
+ for (int j : unmatchedExpected) {
+ builder.addPairResult(
+ compareRepeatedFieldElementPair(
+ /*actual=*/ null,
+ expectedList.get(j),
+ excludeNonRecursive,
+ fieldDescriptor,
+ /*actualFieldIndex=*/ null,
+ j,
+ config));
+ }
+
+ return builder.build();
+ }
+
+ private RepeatedField compareRepeatedFieldExpectingSubsequence(
+ List<?> actualList,
+ List<?> expectedList,
+ boolean excludeNonRecursive,
+ FieldDescriptor fieldDescriptor,
+ FluentEqualityConfig config) {
+ RepeatedField.Builder builder =
+ RepeatedField.newBuilder()
+ .setFieldDescriptor(fieldDescriptor)
+ .setActual(actualList)
+ .setExpected(expectedList);
+
+ // Search for expectedList as a subsequence of actualList.
+ //
+ // This mostly replicates the algorithm used by IterableSubject.containsAtLeast().inOrder(), but
+ // with some tweaks for fuzzy equality and structured output.
+ Deque<Integer> actualIndices = new ArrayDeque<>();
+ for (int i = 0; i < actualList.size(); i++) {
+ actualIndices.addLast(i);
+ }
+ Deque<Integer> actualNotInOrder = new ArrayDeque<>();
+
+ for (int expectedIndex = 0; expectedIndex < expectedList.size(); expectedIndex++) {
+ Object expected = expectedList.get(expectedIndex);
+
+ // Find the first actual element which matches.
+ RepeatedField.PairResult matchingResult =
+ findMatchingPairResult(
+ actualIndices,
+ actualList,
+ expectedIndex,
+ expected,
+ excludeNonRecursive,
+ fieldDescriptor,
+ config);
+
+ if (matchingResult != null) {
+ // Move all prior elements to actualNotInOrder.
+ while (!actualIndices.isEmpty()
+ && actualIndices.getFirst() < matchingResult.actualFieldIndex().get()) {
+ actualNotInOrder.add(actualIndices.removeFirst());
+ }
+ builder.addPairResult(matchingResult);
+ } else {
+ // Otherwise, see if a previous element matches, so we can improve the diff.
+ matchingResult =
+ findMatchingPairResult(
+ actualNotInOrder,
+ actualList,
+ expectedIndex,
+ expected,
+ excludeNonRecursive,
+ fieldDescriptor,
+ config);
+ if (matchingResult != null) {
+ // Report an out-of-order match, which is treated as not-matched.
+ matchingResult = matchingResult.toBuilder().setResult(Result.MOVED_OUT_OF_ORDER).build();
+ builder.addPairResult(matchingResult);
+ } else {
+ // Report a missing expected element.
+ builder.addPairResult(
+ RepeatedField.PairResult.newBuilder()
+ .setResult(Result.REMOVED)
+ .setFieldDescriptor(fieldDescriptor)
+ .setExpected(expected)
+ .setExpectedFieldIndex(expectedIndex)
+ .build());
+ }
+ }
+ }
+
+ // Report any remaining not-in-order elements as ignored.
+ for (int index : actualNotInOrder) {
+ builder.addPairResult(
+ RepeatedField.PairResult.newBuilder()
+ .setResult(Result.IGNORED)
+ .setFieldDescriptor(fieldDescriptor)
+ .setActual(actualList.get(index))
+ .setActualFieldIndex(index)
+ .build());
+ }
+
+ return builder.build();
+ }
+
+ // Given a list of values, a list of indexes into that list, and an expected value, find the first
+ // actual value that compares equal to the expected value, and return the PairResult for it.
+ // Also removes the index for the matching value from actualIndicies.
+ //
+ // If there is no match, returns null.
+ private RepeatedField./*@Nullable*/ PairResult findMatchingPairResult(
+ Deque<Integer> actualIndices,
+ List<?> actualValues,
+ int expectedIndex,
+ Object expectedValue,
+ boolean excludeNonRecursive,
+ FieldDescriptor fieldDescriptor,
+ FluentEqualityConfig config) {
+ Iterator<Integer> actualIndexIter = actualIndices.iterator();
+ while (actualIndexIter.hasNext()) {
+ int actualIndex = actualIndexIter.next();
+ RepeatedField.PairResult pairResult =
+ compareRepeatedFieldElementPair(
+ actualValues.get(actualIndex),
+ expectedValue,
+ excludeNonRecursive,
+ fieldDescriptor,
+ actualIndex,
+ expectedIndex,
+ config);
+ if (pairResult.isMatched()) {
+ actualIndexIter.remove();
+ return pairResult;
+ }
+ }
+
+ return null;
+ }
+
+ private RepeatedField.PairResult compareRepeatedFieldElementPair(
+ @Nullable Object actual,
+ @Nullable Object expected,
+ boolean excludeNonRecursive,
+ FieldDescriptor fieldDescriptor,
+ @Nullable Integer actualFieldIndex,
+ @Nullable Integer expectedFieldIndex,
+ FluentEqualityConfig config) {
+ SingularField comparison =
+ compareSingularValue(
+ actual,
+ expected,
+ /*defaultValue=*/ null,
+ excludeNonRecursive,
+ fieldDescriptor,
+ "<no field path>",
+ config);
+
+ RepeatedField.PairResult.Builder pairResultBuilder =
+ RepeatedField.PairResult.newBuilder()
+ .setResult(comparison.result())
+ .setFieldDescriptor(fieldDescriptor);
+ if (actual != null) {
+ pairResultBuilder.setActual(actual).setActualFieldIndex(actualFieldIndex);
+ }
+ if (expected != null) {
+ pairResultBuilder.setExpected(expected).setExpectedFieldIndex(expectedFieldIndex);
+ }
+ if (comparison.breakdown().isPresent()) {
+ pairResultBuilder.setBreakdown(comparison.breakdown().get());
+ }
+ return pairResultBuilder.build();
+ }
+
+ /** Returns a {@link LinkedHashSet} containing the integers in {@code [0, max)}, in order. */
+ private static Set<Integer> setForRange(int max) {
+ Set<Integer> set = Sets.newLinkedHashSet();
+ for (int i = 0; i < max; i++) {
+ set.add(i);
+ }
+ return set;
+ }
+
+ /**
+ * Compares {@code actualList} and {@code expectedList}, two submessages corresponding to {@code
+ * fieldDescriptor}. Uses {@code excludeNonRecursive}, {@code parentFieldPath}, and {@code
+ * fieldScopeLogic} to compare the messages.
+ *
+ * @return A list in index order, containing the diff results for each message.
+ */
+ private List<SingularField> compareRepeatedFieldByIndices(
+ List<?> actualList,
+ List<?> expectedList,
+ boolean excludeNonRecursive,
+ FieldDescriptor fieldDescriptor,
+ FluentEqualityConfig config) {
+ int maxSize = Math.max(actualList.size(), expectedList.size());
+ ImmutableList.Builder<SingularField> builder = ImmutableList.builderWithExpectedSize(maxSize);
+ for (int i = 0; i < maxSize; i++) {
+ @Nullable Object actual = actualList.size() > i ? actualList.get(i) : null;
+ @Nullable Object expected = expectedList.size() > i ? expectedList.get(i) : null;
+ builder.add(
+ compareSingularValue(
+ actual,
+ expected,
+ /*defaultValue=*/ null,
+ excludeNonRecursive,
+ fieldDescriptor,
+ indexedName(fieldDescriptor, i),
+ config));
+ }
+
+ return builder.build();
+ }
+
+ private SingularField compareSingularValue(
+ @Nullable Object actual,
+ @Nullable Object expected,
+ @Nullable Object defaultValue,
+ boolean excludeNonRecursive,
+ FieldDescriptor fieldDescriptor,
+ String fieldName,
+ FluentEqualityConfig config) {
+ if (fieldDescriptor.getJavaType() == JavaType.MESSAGE) {
+ return compareSingularMessage(
+ (Message) actual,
+ (Message) expected,
+ (Message) defaultValue,
+ excludeNonRecursive,
+ fieldDescriptor,
+ fieldName,
+ config);
+ } else if (excludeNonRecursive) {
+ return SingularField.ignored(fieldName);
+ } else {
+ return compareSingularPrimitive(
+ actual, expected, defaultValue, fieldDescriptor, fieldName, config);
+ }
+ }
+
+ // Replaces 'input' with 'defaultValue' iff input is null and we're ignoring field absence.
+ // Otherwise, just returns the input.
+ private <T> T orIfIgnoringFieldAbsence(
+ @Nullable T input, @Nullable T defaultValue, boolean ignoreFieldAbsence) {
+ return (input == null && ignoreFieldAbsence) ? defaultValue : input;
+ }
+
+ // Returns 'input' if it's non-null, otherwise the default instance of 'other'.
+ // Requires at least one parameter is non-null.
+ private static Message orDefaultForType(@Nullable Message input, @Nullable Message other) {
+ return (input != null) ? input : other.getDefaultInstanceForType();
+ }
+
+ private SingularField compareSingularMessage(
+ @Nullable Message actual,
+ @Nullable Message expected,
+ @Nullable Message defaultValue,
+ boolean excludeNonRecursive,
+ FieldDescriptor fieldDescriptor,
+ String fieldName,
+ FluentEqualityConfig config) {
+ Result.Builder result = Result.builder();
+
+ // Use the default if it's set and we're ignoring field absence.
+ boolean ignoreFieldAbsence =
+ config.ignoreFieldAbsenceScope().contains(rootDescriptor, SubScopeId.of(fieldDescriptor));
+ actual = orIfIgnoringFieldAbsence(actual, defaultValue, ignoreFieldAbsence);
+ expected = orIfIgnoringFieldAbsence(expected, defaultValue, ignoreFieldAbsence);
+
+ // If actual or expected is missing here, we know our result so long as it's not ignored.
+ result.markRemovedIf(actual == null);
+ result.markAddedIf(expected == null);
+
+ // Perform the detailed breakdown only if necessary.
+ @Nullable DiffResult breakdown = null;
+ if (result.build() == Result.MATCHED || excludeNonRecursive) {
+ actual = orDefaultForType(actual, expected);
+ expected = orDefaultForType(expected, actual);
+
+ breakdown = diffMessages(actual, expected, config);
+ if (breakdown.isIgnored() && excludeNonRecursive) {
+ // Ignore this field entirely, report nothing.
+ return SingularField.ignored(fieldName);
+ }
+
+ result.markModifiedIf(!breakdown.isMatched());
+ }
+
+ // Report the full breakdown.
+ SingularField.Builder singularFieldBuilder =
+ SingularField.newBuilder()
+ .setSubScopeId(SubScopeId.of(fieldDescriptor))
+ .setFieldName(fieldName)
+ .setResult(result.build());
+ if (actual != null) {
+ singularFieldBuilder.setActual(actual);
+ }
+ if (expected != null) {
+ singularFieldBuilder.setExpected(expected);
+ }
+ if (breakdown != null) {
+ singularFieldBuilder.setBreakdown(breakdown);
+ }
+ return singularFieldBuilder.build();
+ }
+
+ private SingularField compareSingularPrimitive(
+ @Nullable Object actual,
+ @Nullable Object expected,
+ @Nullable Object defaultValue,
+ FieldDescriptor fieldDescriptor,
+ String fieldName,
+ FluentEqualityConfig config) {
+ Result.Builder result = Result.builder();
+
+ // Use the default if it's set and we're ignoring field absence, or if it's a Proto3 primitive
+ // for which default is indistinguishable from unset.
+ SubScopeId subScopeId = SubScopeId.of(fieldDescriptor);
+ boolean isNonRepeatedProto3 =
+ !fieldDescriptor.isRepeated()
+ && fieldDescriptor.getContainingOneof() == null
+ && fieldDescriptor.getFile().getSyntax() == Syntax.PROTO3;
+ boolean ignoreFieldAbsence =
+ isNonRepeatedProto3
+ || config.ignoreFieldAbsenceScope().contains(rootDescriptor, subScopeId);
+ actual = orIfIgnoringFieldAbsence(actual, defaultValue, ignoreFieldAbsence);
+ expected = orIfIgnoringFieldAbsence(expected, defaultValue, ignoreFieldAbsence);
+
+ // If actual or expected is missing here, we know our result.
+ result.markRemovedIf(actual == null);
+ result.markAddedIf(expected == null);
+
+ if (actual != null && expected != null) {
+ if (actual instanceof Double) {
+ result.markModifiedIf(
+ !doublesEqual(
+ (double) actual,
+ (double) expected,
+ config.doubleCorrespondenceMap().get(rootDescriptor, subScopeId)
+ ));
+ } else if (actual instanceof Float) {
+ result.markModifiedIf(
+ !floatsEqual(
+ (float) actual,
+ (float) expected,
+ config.floatCorrespondenceMap().get(rootDescriptor, subScopeId)
+ ));
+ } else {
+ result.markModifiedIf(!Objects.equal(actual, expected));
+ }
+ }
+
+ SingularField.Builder singularFieldBuilder =
+ SingularField.newBuilder()
+ .setSubScopeId(SubScopeId.of(fieldDescriptor))
+ .setFieldName(fieldName)
+ .setResult(result.build());
+ if (actual != null) {
+ singularFieldBuilder.setActual(actual);
+ }
+ if (expected != null) {
+ singularFieldBuilder.setExpected(expected);
+ }
+ return singularFieldBuilder.build();
+ }
+
+ private boolean doublesEqual(
+ double x,
+ double y,
+ Optional<Correspondence<Number, Number>> correspondence
+ ) {
+ if (correspondence.isPresent()) {
+ return correspondence.get().compare(x, y);
+ } else {
+ return Double.compare(x, y) == 0;
+ }
+ }
+
+ private boolean floatsEqual(
+ float x,
+ float y,
+ Optional<Correspondence<Number, Number>> correspondence
+ ) {
+ if (correspondence.isPresent()) {
+ return correspondence.get().compare(x, y);
+ } else {
+ return Float.compare(x, y) == 0;
+ }
+ }
+
+ private UnknownFieldSetDiff diffUnknowns(
+ UnknownFieldSet actual, UnknownFieldSet expected, FluentEqualityConfig config) {
+ UnknownFieldSetDiff.Builder builder = UnknownFieldSetDiff.newBuilder();
+
+ Map<Integer, UnknownFieldSet.Field> actualFields = actual.asMap();
+ Map<Integer, UnknownFieldSet.Field> expectedFields = expected.asMap();
+ for (int fieldNumber : Sets.union(actualFields.keySet(), expectedFields.keySet())) {
+ UnknownFieldSet.Field actualField = actualFields.get(fieldNumber);
+ UnknownFieldSet.Field expectedField = expectedFields.get(fieldNumber);
+ for (UnknownFieldDescriptor.Type type : UnknownFieldDescriptor.Type.all()) {
+ List<?> actualValues =
+ actualField != null ? type.getValues(actualField) : Collections.emptyList();
+ List<?> expectedValues =
+ expectedField != null ? type.getValues(expectedField) : Collections.emptyList();
+ if (actualValues.isEmpty() && expectedValues.isEmpty()) {
+ continue;
+ }
+
+ UnknownFieldDescriptor unknownFieldDescriptor =
+ UnknownFieldDescriptor.create(fieldNumber, type);
+ SubScopeId subScopeId = SubScopeId.of(unknownFieldDescriptor);
+ FieldScopeResult compareFields =
+ config.compareFieldsScope().policyFor(rootDescriptor, subScopeId);
+ if (compareFields == FieldScopeResult.EXCLUDED_RECURSIVELY) {
+ builder.addSingularField(
+ fieldNumber, SingularField.ignored(name(unknownFieldDescriptor)));
+ continue;
+ }
+
+ builder.addAllSingularFields(
+ fieldNumber,
+ compareUnknownFieldList(
+ actualValues,
+ expectedValues,
+ compareFields == FieldScopeResult.EXCLUDED_NONRECURSIVELY,
+ unknownFieldDescriptor,
+ config.subScope(rootDescriptor, subScopeId)));
+ }
+ }
+
+ return builder.build();
+ }
+
+ private List<SingularField> compareUnknownFieldList(
+ List<?> actualValues,
+ List<?> expectedValues,
+ boolean excludeNonRecursive,
+ UnknownFieldDescriptor unknownFieldDescriptor,
+ FluentEqualityConfig config) {
+ int maxSize = Math.max(actualValues.size(), expectedValues.size());
+ ImmutableList.Builder<SingularField> builder = ImmutableList.builderWithExpectedSize(maxSize);
+ for (int i = 0; i < maxSize; i++) {
+ @Nullable Object actual = actualValues.size() > i ? actualValues.get(i) : null;
+ @Nullable Object expected = expectedValues.size() > i ? expectedValues.get(i) : null;
+ builder.add(
+ compareUnknownFieldValue(
+ actual,
+ expected,
+ excludeNonRecursive,
+ unknownFieldDescriptor,
+ indexedName(unknownFieldDescriptor, i),
+ config));
+ }
+
+ return builder.build();
+ }
+
+ private SingularField compareUnknownFieldValue(
+ @Nullable Object actual,
+ @Nullable Object expected,
+ boolean excludeNonRecursive,
+ UnknownFieldDescriptor unknownFieldDescriptor,
+ String fieldName,
+ FluentEqualityConfig config) {
+ if (unknownFieldDescriptor.type() == UnknownFieldDescriptor.Type.GROUP) {
+ return compareUnknownFieldSet(
+ (UnknownFieldSet) actual,
+ (UnknownFieldSet) expected,
+ excludeNonRecursive,
+ unknownFieldDescriptor,
+ fieldName,
+ config);
+ } else {
+ checkState(!excludeNonRecursive, "excludeNonRecursive is not a valid for primitives.");
+ return compareUnknownPrimitive(actual, expected, unknownFieldDescriptor, fieldName);
+ }
+ }
+
+ private SingularField compareUnknownFieldSet(
+ @Nullable UnknownFieldSet actual,
+ @Nullable UnknownFieldSet expected,
+ boolean excludeNonRecursive,
+ UnknownFieldDescriptor unknownFieldDescriptor,
+ String fieldName,
+ FluentEqualityConfig config) {
+ Result.Builder result = Result.builder();
+
+ // If actual or expected is missing, we know the result as long as it's not ignored.
+ result.markRemovedIf(actual == null);
+ result.markAddedIf(expected == null);
+
+ // Perform the detailed breakdown only if necessary.
+ @Nullable UnknownFieldSetDiff unknownsBreakdown = null;
+ if (result.build() == Result.MATCHED || excludeNonRecursive) {
+ actual = firstNonNull(actual, UnknownFieldSet.getDefaultInstance());
+ expected = firstNonNull(expected, UnknownFieldSet.getDefaultInstance());
+
+ unknownsBreakdown = diffUnknowns(actual, expected, config);
+ if (unknownsBreakdown.isIgnored() && excludeNonRecursive) {
+ // Ignore this field entirely, report nothing.
+ return SingularField.ignored(fieldName);
+ }
+ result.markModifiedIf(!unknownsBreakdown.isMatched());
+ }
+
+ // Report the full breakdown.
+ SingularField.Builder singularFieldBuilder =
+ SingularField.newBuilder()
+ .setSubScopeId(SubScopeId.of(unknownFieldDescriptor))
+ .setFieldName(fieldName)
+ .setResult(result.build());
+ if (actual != null) {
+ singularFieldBuilder.setActual(actual);
+ }
+ if (expected != null) {
+ singularFieldBuilder.setExpected(expected);
+ }
+ if (unknownsBreakdown != null) {
+ singularFieldBuilder.setUnknownsBreakdown(unknownsBreakdown);
+ }
+ return singularFieldBuilder.build();
+ }
+
+ private SingularField compareUnknownPrimitive(
+ @Nullable Object actual,
+ @Nullable Object expected,
+ UnknownFieldDescriptor unknownFieldDescriptor,
+ String fieldName) {
+ Result.Builder result = Result.builder();
+
+ result.markRemovedIf(actual == null);
+ result.markAddedIf(expected == null);
+ result.markModifiedIf(!Objects.equal(actual, expected));
+
+ SingularField.Builder singularFieldBuilder =
+ SingularField.newBuilder()
+ .setSubScopeId(SubScopeId.of(unknownFieldDescriptor))
+ .setFieldName(fieldName)
+ .setResult(result.build());
+ if (actual != null) {
+ singularFieldBuilder.setActual(actual);
+ }
+ if (expected != null) {
+ singularFieldBuilder.setExpected(expected);
+ }
+ return singularFieldBuilder.build();
+ }
+
+ private static String name(FieldDescriptor fieldDescriptor) {
+ return fieldDescriptor.isExtension() ? "[" + fieldDescriptor + "]" : fieldDescriptor.getName();
+ }
+
+ private static String name(UnknownFieldDescriptor unknownFieldDescriptor) {
+ return String.valueOf(unknownFieldDescriptor.fieldNumber());
+ }
+
+ private static String indexedName(
+ FieldDescriptor fieldDescriptor, Object key, FieldDescriptor keyFieldDescriptor) {
+ StringBuilder sb = new StringBuilder();
+ try {
+ TextFormat.printFieldValue(keyFieldDescriptor, key, sb);
+ } catch (IOException impossible) {
+ throw new AssertionError(impossible);
+ }
+ return name(fieldDescriptor) + "[" + sb + "]";
+ }
+
+ private static String indexedName(FieldDescriptor fieldDescriptor, int index) {
+ return name(fieldDescriptor) + "[" + index + "]";
+ }
+
+ private static String indexedName(UnknownFieldDescriptor unknownFieldDescriptor, int index) {
+ return name(unknownFieldDescriptor) + "[" + index + "]";
+ }
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/RecursableDiffEntity.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/RecursableDiffEntity.java
new file mode 100644
index 00000000..54605c91
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/RecursableDiffEntity.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (c) 2017 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+/**
+ * A generic entity in the {@link DiffResult} tree with queryable properties.
+ *
+ * <p>This class is not directly extensible. Only the inner classes, {@link
+ * RecursableDiffEntity.WithoutResultCode} and {@link RecursableDiffEntity.WithResultCode}, can be
+ * extended.
+ *
+ * <p>A {@code RecursableDiffEntity}'s base properties (i.e., {@link #isMatched()}, {@link
+ * #isIgnored()}) are determined differently depending on the subtype. The {@code WithoutResultCode}
+ * subtype derives its base properties entirely from its children, while the {@code WithResultCode}
+ * subtype derives its base properties from an enum explicitly set on the entity. The {@link
+ * #isAnyChildMatched()} and {@link #isAnyChildIgnored()} properties are determined recursively on
+ * both subtypes.
+ *
+ * <p>A {@code RecursableDiffEntity} may have no children. The nature and count of an entity's
+ * children depends on the implementation - see {@link DiffResult} for concrete instances.
+ */
+abstract class RecursableDiffEntity {
+
+ // Lazily-initialized return values for the recursive properties of the entity.
+ // null = not initialized yet
+ //
+ // This essentially implements what @Memoized does, but @AutoValue doesn't support @Memoized on
+ // parent classes. I think it's better to roll-our-own in the parent class to take advantage of
+ // inheritance, than to duplicate the @Memoized methods for every subclass.
+
+ private Boolean isAnyChildIgnored = null;
+ private Boolean isAnyChildMatched = null;
+
+ // Only extended by inner classes.
+ private RecursableDiffEntity() {}
+
+ /**
+ * The children of this entity. May be empty.
+ *
+ * <p>Subclasses should {@link @Memoized} this method especially if it's expensive.
+ */
+ abstract Iterable<? extends RecursableDiffEntity> childEntities();
+
+ /** Returns whether or not the two entities matched according to the diff rules. */
+ abstract boolean isMatched();
+
+ /** Returns true if all sub-fields of both entities were ignored for comparison. */
+ abstract boolean isIgnored();
+
+ /**
+ * Returns true if some child entity matched.
+ *
+ * <p>Caches the result for future calls.
+ */
+ final boolean isAnyChildMatched() {
+ if (isAnyChildMatched == null) {
+ isAnyChildMatched = false;
+ for (RecursableDiffEntity entity : childEntities()) {
+ if ((entity.isMatched() && !entity.isContentEmpty()) || entity.isAnyChildMatched()) {
+ isAnyChildMatched = true;
+ break;
+ }
+ }
+ }
+ return isAnyChildMatched;
+ }
+
+ /**
+ * Returns true if some child entity was ignored.
+ *
+ * <p>Caches the result for future calls.
+ */
+ final boolean isAnyChildIgnored() {
+ if (isAnyChildIgnored == null) {
+ isAnyChildIgnored = false;
+ for (RecursableDiffEntity entity : childEntities()) {
+ if ((entity.isIgnored() && !entity.isContentEmpty()) || entity.isAnyChildIgnored()) {
+ isAnyChildIgnored = true;
+ break;
+ }
+ }
+ }
+ return isAnyChildIgnored;
+ }
+
+ /**
+ * Prints the contents of this diff entity to {@code sb}.
+ *
+ * @param includeMatches Whether to include reports for fields which matched.
+ * @param fieldPrefix The human-readable field path leading to this entity. Empty if this is the
+ * root entity.
+ * @param sb Builder to print the text to.
+ */
+ abstract void printContents(boolean includeMatches, String fieldPrefix, StringBuilder sb);
+
+ /** Returns true if this entity has no contents to print, with or without includeMatches. */
+ abstract boolean isContentEmpty();
+
+ final void printChildContents(boolean includeMatches, String fieldPrefix, StringBuilder sb) {
+ for (RecursableDiffEntity entity : childEntities()) {
+ entity.printContents(includeMatches, fieldPrefix, sb);
+ }
+ }
+
+ /**
+ * A generic entity in the {@link DiffResult} tree without a result code.
+ *
+ * <p>This entity derives its {@code isMatched()} and {@code isIgnored()} state purely from its
+ * children. If it has no children, it is considered both matched and ignored.
+ */
+ abstract static class WithoutResultCode extends RecursableDiffEntity {
+
+ private Boolean isMatched = null;
+ private Boolean isIgnored = null;
+
+ @Override
+ final boolean isMatched() {
+ if (isMatched == null) {
+ isMatched = true;
+ for (RecursableDiffEntity entity : childEntities()) {
+ if (!entity.isMatched()) {
+ isMatched = false;
+ break;
+ }
+ }
+ }
+ return isMatched;
+ }
+
+ @Override
+ final boolean isIgnored() {
+ if (isIgnored == null) {
+ isIgnored = true;
+ for (RecursableDiffEntity entity : childEntities()) {
+ if (!entity.isIgnored()) {
+ isIgnored = false;
+ break;
+ }
+ }
+ }
+ return isIgnored;
+ }
+ }
+
+ /**
+ * A generic entity in the {@link DiffResult} tree with a result code.
+ *
+ * <p>The result code overrides {@code isMatched()} and {@code isIgnored()} evaluation, using the
+ * provided enum instead of any child states.
+ */
+ abstract static class WithResultCode extends RecursableDiffEntity {
+ enum Result {
+ /** No differences. The expected case. */
+ MATCHED,
+
+ /** expected() didn't have this field, actual() did. */
+ ADDED,
+
+ /** actual() didn't have this field, expected() did. */
+ REMOVED,
+
+ /** Both messages had the field but the values don't match. */
+ MODIFIED,
+
+ /**
+ * The message was moved from one index to another, but strict ordering was expected.
+ *
+ * <p>This is only possible on {@link DiffResult.RepeatedField.PairResult}.
+ */
+ MOVED_OUT_OF_ORDER,
+
+ /**
+ * The messages were ignored for the sake of comparison.
+ *
+ * <p>IGNORED fields should also be considered MATCHED, for the sake of pass/fail decisions.
+ * The IGNORED information is useful for limiting diff output: i.e., if all fields in a deep
+ * submessage-to-submessage comparison are ignored, we can print the top-level type as ignored
+ * and omit diff lines for the rest of the fields within.
+ */
+ IGNORED;
+
+ static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * A helper class for computing a {@link Result}. It defaults to {@code MATCHED}, but can be
+ * changed exactly once if called with a true {@code condition}.
+ *
+ * <p>All subsequent 'mark' calls after a successful mark are ignored.
+ */
+ static final class Builder {
+ private Result state = Result.MATCHED;
+
+ private Builder() {}
+
+ public void markAddedIf(boolean condition) {
+ setIf(condition, Result.ADDED);
+ }
+
+ public void markRemovedIf(boolean condition) {
+ setIf(condition, Result.REMOVED);
+ }
+
+ public void markModifiedIf(boolean condition) {
+ setIf(condition, Result.MODIFIED);
+ }
+
+ public Result build() {
+ return state;
+ }
+
+ private void setIf(boolean condition, Result newState) {
+ if (condition && state == Result.MATCHED) {
+ state = newState;
+ }
+ }
+ }
+ }
+
+ abstract Result result();
+
+ @Override
+ final boolean isMatched() {
+ return result() == Result.MATCHED || result() == Result.IGNORED;
+ }
+
+ @Override
+ final boolean isIgnored() {
+ return result() == Result.IGNORED;
+ }
+ }
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/SubScopeId.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/SubScopeId.java
new file mode 100644
index 00000000..4860969e
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/SubScopeId.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2018 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import com.google.auto.value.AutoOneOf;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+
+@AutoOneOf(SubScopeId.Kind.class)
+abstract class SubScopeId {
+ enum Kind {
+ FIELD_DESCRIPTOR,
+ UNKNOWN_FIELD_DESCRIPTOR;
+ }
+
+ abstract Kind kind();
+
+ abstract FieldDescriptor fieldDescriptor();
+
+ abstract UnknownFieldDescriptor unknownFieldDescriptor();
+
+ /** Returns a short, human-readable version of this identifier. */
+ final String shortName() {
+ switch (kind()) {
+ case FIELD_DESCRIPTOR:
+ return fieldDescriptor().isExtension()
+ ? "[" + fieldDescriptor() + "]"
+ : fieldDescriptor().getName();
+ case UNKNOWN_FIELD_DESCRIPTOR:
+ return String.valueOf(unknownFieldDescriptor().fieldNumber());
+ }
+ throw new AssertionError(kind());
+ }
+
+ static SubScopeId of(FieldDescriptor fieldDescriptor) {
+ return AutoOneOf_SubScopeId.fieldDescriptor(fieldDescriptor);
+ }
+
+ static SubScopeId of(UnknownFieldDescriptor unknownFieldDescriptor) {
+ return AutoOneOf_SubScopeId.unknownFieldDescriptor(unknownFieldDescriptor);
+ }
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/UnknownFieldDescriptor.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/UnknownFieldDescriptor.java
new file mode 100644
index 00000000..3b45242f
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/UnknownFieldDescriptor.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2018 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.UnknownFieldSet;
+import com.google.protobuf.WireFormat;
+import java.util.List;
+
+/** Convenience class encapsulating type information for unknown fields. */
+@AutoValue
+abstract class UnknownFieldDescriptor {
+
+ enum Type {
+ VARINT(WireFormat.WIRETYPE_VARINT) {
+ @Override
+ public List<?> getValues(UnknownFieldSet.Field field) {
+ return field.getVarintList();
+ }
+ },
+ FIXED32(WireFormat.WIRETYPE_FIXED32) {
+ @Override
+ public List<?> getValues(UnknownFieldSet.Field field) {
+ return field.getFixed32List();
+ }
+ },
+ FIXED64(WireFormat.WIRETYPE_FIXED64) {
+ @Override
+ public List<?> getValues(UnknownFieldSet.Field field) {
+ return field.getFixed64List();
+ }
+ },
+ LENGTH_DELIMITED(WireFormat.WIRETYPE_LENGTH_DELIMITED) {
+ @Override
+ public List<?> getValues(UnknownFieldSet.Field field) {
+ return field.getLengthDelimitedList();
+ }
+ },
+ GROUP(WireFormat.WIRETYPE_START_GROUP) {
+ @Override
+ public List<?> getValues(UnknownFieldSet.Field field) {
+ return field.getGroupList();
+ }
+ };
+
+ private static final ImmutableList<Type> TYPES = ImmutableList.copyOf(values());
+
+ static ImmutableList<Type> all() {
+ return TYPES;
+ }
+
+ private final int wireType;
+
+ Type(int wireType) {
+ this.wireType = wireType;
+ }
+
+ /** Returns the corresponding values from the given field. */
+ abstract List<?> getValues(UnknownFieldSet.Field field);
+
+ /** Returns the {@link WireFormat} constant for this field type. */
+ final int wireType() {
+ return wireType;
+ }
+ }
+
+ static UnknownFieldDescriptor create(int fieldNumber, Type type) {
+ return new AutoValue_UnknownFieldDescriptor(fieldNumber, type);
+ }
+
+ abstract int fieldNumber();
+
+ abstract Type type();
+
+ static ImmutableList<UnknownFieldDescriptor> descriptors(
+ int fieldNumber, UnknownFieldSet.Field field) {
+ ImmutableList.Builder<UnknownFieldDescriptor> builder = ImmutableList.builder();
+ for (Type type : Type.all()) {
+ if (!type.getValues(field).isEmpty()) {
+ builder.add(create(fieldNumber, type));
+ }
+ }
+ return builder.build();
+ }
+}
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/package-info.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/package-info.java
new file mode 100644
index 00000000..a76e2e23
--- /dev/null
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/package-info.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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.
+ */
+
+/**
+ * Custom subjects for testing <a href="https://developers.google.com/protocol-buffers/">Protocol
+ * Buffer</a> instances.
+ *
+ * <p>This package is a part of the open-source <a href="https://github.com/google/truth">Truth</a>
+ * project.
+ */
+@CheckReturnValue
+package com.google.common.truth.extensions.proto;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/FieldScopesTest.java b/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/FieldScopesTest.java
new file mode 100644
index 00000000..fb0a07de
--- /dev/null
+++ b/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/FieldScopesTest.java
@@ -0,0 +1,1109 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Lists;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.Message;
+import com.google.protobuf.UnknownFieldSet;
+import com.google.protobuf.UnknownFieldSet.Field;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/** Unit tests for {@link FieldScope}, and their interaction with {@link ProtoSubject}. */
+@RunWith(Parameterized.class)
+public class FieldScopesTest extends ProtoSubjectTestBase {
+
+ @Parameters(name = "{0}")
+ public static Collection<Object[]> parameters() {
+ return ProtoSubjectTestBase.parameters();
+ }
+
+ // Set up for the ignoringTopLevelField tests.
+ // ignoringFieldMessage and ignoringFieldDiffMessage are simple messages with two fields set. They
+ // are the same for the "good" field, and different for the "bad" field. The *FieldNumber and
+ // *FieldDescriptor members point to these fields.
+
+ private final Message ignoringFieldMessage;
+ private final Message ignoringFieldDiffMessage;
+ private final int goodFieldNumber;
+ private final int badFieldNumber;
+ private final FieldDescriptor goodFieldDescriptor;
+ private final FieldDescriptor badFieldDescriptor;
+
+ public FieldScopesTest(TestType testType) {
+ super(testType);
+
+ ignoringFieldMessage = parse("o_int: 3 r_string: \"foo\"");
+ ignoringFieldDiffMessage = parse("o_int: 3 r_string: \"bar\"");
+ goodFieldNumber = getFieldNumber("o_int");
+ badFieldNumber = getFieldNumber("r_string");
+ goodFieldDescriptor = getFieldDescriptor("o_int");
+ badFieldDescriptor = getFieldDescriptor("r_string");
+ }
+
+ @Test
+ public void testUnequalMessages() {
+ Message message = parse("o_int: 3 r_string: \"foo\"");
+ Message diffMessage = parse("o_int: 5 r_string: \"bar\"");
+
+ expectThat(diffMessage).isNotEqualTo(message);
+ }
+
+ @Test
+ public void testFieldScopes_all() {
+ Message message = parse("o_int: 3 r_string: \"foo\"");
+ Message diffMessage = parse("o_int: 5 r_string: \"bar\"");
+
+ expectThat(diffMessage).withPartialScope(FieldScopes.all()).isNotEqualTo(message);
+ expectThat(diffMessage).ignoringFieldScope(FieldScopes.all()).isEqualTo(message);
+
+ expectFailureWhenTesting()
+ .that(diffMessage)
+ .ignoringFieldScope(FieldScopes.all())
+ .isNotEqualTo(message);
+ expectIsNotEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("ignored: o_int");
+ expectThatFailure().hasMessageThat().contains("ignored: r_string");
+ }
+
+ @Test
+ public void testFieldScopes_none() {
+ Message message = parse("o_int: 3 r_string: \"foo\"");
+ Message diffMessage = parse("o_int: 5 r_string: \"bar\"");
+
+ expectThat(diffMessage).ignoringFieldScope(FieldScopes.none()).isNotEqualTo(message);
+ expectThat(diffMessage).withPartialScope(FieldScopes.none()).isEqualTo(message);
+
+ expectFailureWhenTesting()
+ .that(diffMessage)
+ .withPartialScope(FieldScopes.none())
+ .isNotEqualTo(message);
+ expectIsNotEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("ignored: o_int");
+ expectThatFailure().hasMessageThat().contains("ignored: r_string");
+ }
+
+ @Test
+ public void testFieldScopes_none_withAnyField() {
+ String typeUrl =
+ isProto3()
+ ? "type.googleapis.com/com.google.common.truth.extensions.proto.SubTestMessage3"
+ : "type.googleapis.com/com.google.common.truth.extensions.proto.SubTestMessage2";
+ Message message = parse("o_int: 3 o_any_message { [" + typeUrl + "]: { r_string: \"foo\" } }");
+ Message diffMessage =
+ parse("o_int: 5 o_any_message { [" + typeUrl + "]: { r_string: \"bar\" } }");
+
+ expectThat(diffMessage).ignoringFieldScope(FieldScopes.none()).isNotEqualTo(message);
+ expectThat(diffMessage).withPartialScope(FieldScopes.none()).isEqualTo(message);
+
+ expectFailureWhenTesting()
+ .that(diffMessage)
+ .withPartialScope(FieldScopes.none())
+ .isNotEqualTo(message);
+ expectIsNotEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("ignored: o_int");
+ expectThatFailure().hasMessageThat().contains("ignored: o_any_message");
+ }
+
+ @Test
+ public void testIgnoringTopLevelField_ignoringField() {
+ expectThat(ignoringFieldDiffMessage)
+ .ignoringFields(goodFieldNumber)
+ .isNotEqualTo(ignoringFieldMessage);
+ expectThat(ignoringFieldDiffMessage)
+ .ignoringFields(badFieldNumber)
+ .isEqualTo(ignoringFieldMessage);
+
+ expectFailureWhenTesting()
+ .that(ignoringFieldDiffMessage)
+ .ignoringFields(goodFieldNumber)
+ .isEqualTo(ignoringFieldMessage);
+ expectIsEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("modified: r_string[0]: \"foo\" -> \"bar\"");
+
+ expectFailureWhenTesting()
+ .that(ignoringFieldDiffMessage)
+ .ignoringFields(badFieldNumber)
+ .isNotEqualTo(ignoringFieldMessage);
+ expectIsNotEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("ignored: r_string");
+ }
+
+ @Test
+ public void testIgnoringTopLevelAnyField_ignoringField() {
+ String typeUrl =
+ isProto3()
+ ? "type.googleapis.com/com.google.common.truth.extensions.proto.SubTestMessage3"
+ : "type.googleapis.com/com.google.common.truth.extensions.proto.SubTestMessage2";
+ Message message = parse("o_int: 1 o_any_message { [" + typeUrl + "]: { r_string: \"foo\" } }");
+ Message diffMessage = parse("o_int: 1");
+ int goodFieldNumber = getFieldNumber("o_int");
+ int badFieldNumber = getFieldNumber("o_any_message");
+
+ expectThat(diffMessage).ignoringFields(goodFieldNumber).isNotEqualTo(message);
+ expectThat(diffMessage).ignoringFields(badFieldNumber).isEqualTo(diffMessage);
+
+ expectFailureWhenTesting().that(diffMessage).ignoringFields(goodFieldNumber).isEqualTo(message);
+ expectIsEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("deleted: o_any_message");
+
+ expectFailureWhenTesting()
+ .that(diffMessage)
+ .ignoringFields(badFieldNumber)
+ .isNotEqualTo(message);
+ expectIsNotEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("ignored: o_any_message");
+ }
+
+ @Test
+ public void testIgnoringTopLevelField_fieldScopes_ignoringFields() {
+ expectThat(ignoringFieldDiffMessage)
+ .withPartialScope(FieldScopes.ignoringFields(goodFieldNumber))
+ .isNotEqualTo(ignoringFieldMessage);
+ expectThat(ignoringFieldDiffMessage)
+ .ignoringFieldScope(FieldScopes.ignoringFields(goodFieldNumber))
+ .isEqualTo(ignoringFieldMessage);
+ expectThat(ignoringFieldDiffMessage)
+ .withPartialScope(FieldScopes.ignoringFields(badFieldNumber))
+ .isEqualTo(ignoringFieldMessage);
+ expectThat(ignoringFieldDiffMessage)
+ .ignoringFieldScope(FieldScopes.ignoringFields(badFieldNumber))
+ .isNotEqualTo(ignoringFieldMessage);
+ }
+
+ @Test
+ public void testIgnoringTopLevelField_fieldScopes_allowingFields() {
+ expectThat(ignoringFieldDiffMessage)
+ .withPartialScope(FieldScopes.allowingFields(goodFieldNumber))
+ .isEqualTo(ignoringFieldMessage);
+ expectThat(ignoringFieldDiffMessage)
+ .ignoringFieldScope(FieldScopes.allowingFields(goodFieldNumber))
+ .isNotEqualTo(ignoringFieldMessage);
+ expectThat(ignoringFieldDiffMessage)
+ .withPartialScope(FieldScopes.allowingFields(badFieldNumber))
+ .isNotEqualTo(ignoringFieldMessage);
+ expectThat(ignoringFieldDiffMessage)
+ .ignoringFieldScope(FieldScopes.allowingFields(badFieldNumber))
+ .isEqualTo(ignoringFieldMessage);
+ }
+
+ @Test
+ public void testIgnoringTopLevelAnyField_fieldScopes_allowingFields() {
+ String typeUrl =
+ isProto3()
+ ? "type.googleapis.com/com.google.common.truth.extensions.proto.SubTestMessage3"
+ : "type.googleapis.com/com.google.common.truth.extensions.proto.SubTestMessage2";
+ Message message =
+ parse("o_int: 1 o_any_message { [" + typeUrl + "]: { o_int: 2 r_string: \"foo\" } }");
+ Message diffMessage = parse("o_int: 1");
+ int goodFieldNumber = getFieldNumber("o_int");
+
+ expectThat(message)
+ .withPartialScope(FieldScopes.allowingFields(goodFieldNumber))
+ .isEqualTo(diffMessage);
+ }
+
+ @Test
+ public void testIgnoringTopLevelField_fieldScopes_allowingFieldDescriptors() {
+ expectThat(ignoringFieldDiffMessage)
+ .withPartialScope(FieldScopes.allowingFieldDescriptors(goodFieldDescriptor))
+ .isEqualTo(ignoringFieldMessage);
+ expectThat(ignoringFieldDiffMessage)
+ .ignoringFieldScope(FieldScopes.allowingFieldDescriptors(goodFieldDescriptor))
+ .isNotEqualTo(ignoringFieldMessage);
+ expectThat(ignoringFieldDiffMessage)
+ .withPartialScope(FieldScopes.allowingFieldDescriptors(badFieldDescriptor))
+ .isNotEqualTo(ignoringFieldMessage);
+ expectThat(ignoringFieldDiffMessage)
+ .ignoringFieldScope(FieldScopes.allowingFieldDescriptors(badFieldDescriptor))
+ .isEqualTo(ignoringFieldMessage);
+ }
+
+ @Test
+ public void testIgnoringTopLevelField_fieldScopes_ignoringFieldDescriptors() {
+ expectThat(ignoringFieldDiffMessage)
+ .withPartialScope(FieldScopes.ignoringFieldDescriptors(goodFieldDescriptor))
+ .isNotEqualTo(ignoringFieldMessage);
+ expectThat(ignoringFieldDiffMessage)
+ .ignoringFieldScope(FieldScopes.ignoringFieldDescriptors(goodFieldDescriptor))
+ .isEqualTo(ignoringFieldMessage);
+ expectThat(ignoringFieldDiffMessage)
+ .withPartialScope(FieldScopes.ignoringFieldDescriptors(badFieldDescriptor))
+ .isEqualTo(ignoringFieldMessage);
+ expectThat(ignoringFieldDiffMessage)
+ .ignoringFieldScope(FieldScopes.ignoringFieldDescriptors(badFieldDescriptor))
+ .isNotEqualTo(ignoringFieldMessage);
+ }
+
+ @Test
+ public void testEmptySubMessage() {
+ Message message = parse("o_int: 1 o_sub_test_message: { }");
+ Message eqMessage = parse("o_int: 2 o_sub_test_message: { }");
+ Message diffMessage = parse("o_int: 3");
+
+ // Different logic gets exercised when we add an 'ignore' clause.
+ // Let's ensure o_sub_test_message is compared properly in all cases.
+ int fieldNumber = getFieldNumber("o_int");
+
+ expectThat(eqMessage).isNotEqualTo(message);
+ expectThat(eqMessage).ignoringFieldAbsence().isNotEqualTo(message);
+ expectThat(eqMessage).ignoringFields(fieldNumber).isEqualTo(message);
+ expectThat(eqMessage).ignoringFields(fieldNumber).ignoringFieldAbsence().isEqualTo(message);
+
+ expectThat(diffMessage).isNotEqualTo(message);
+ expectThat(diffMessage).ignoringFieldAbsence().isNotEqualTo(message);
+ expectThat(diffMessage).ignoringFields(fieldNumber).isNotEqualTo(message);
+ expectThat(diffMessage).ignoringFields(fieldNumber).ignoringFieldAbsence().isEqualTo(message);
+ }
+
+ @Test
+ public void testIgnoreSubMessageField() {
+ Message message = parse("o_int: 1 o_sub_test_message: { o_int: 2 }");
+ Message diffMessage = parse("o_int: 2 o_sub_test_message: { o_int: 2 }");
+ Message eqMessage1 = parse("o_int: 1");
+ Message eqMessage2 = parse("o_int: 1 o_sub_test_message: {}");
+ Message eqMessage3 = parse("o_int: 1 o_sub_test_message: { o_int: 3 r_string: \"x\" }");
+ int fieldNumber = getFieldNumber("o_sub_test_message");
+
+ expectThat(diffMessage).ignoringFields(fieldNumber).isNotEqualTo(message);
+ expectThat(eqMessage1).ignoringFields(fieldNumber).isEqualTo(message);
+ expectThat(eqMessage2).ignoringFields(fieldNumber).isEqualTo(message);
+ expectThat(eqMessage3).ignoringFields(fieldNumber).isEqualTo(message);
+
+ expectFailureWhenTesting().that(diffMessage).ignoringFields(fieldNumber).isEqualTo(message);
+ expectIsEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("modified: o_int: 1 -> 2");
+
+ expectFailureWhenTesting().that(eqMessage3).ignoringFields(fieldNumber).isNotEqualTo(message);
+ expectIsNotEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("ignored: o_sub_test_message");
+ }
+
+ @Test
+ public void testIgnoreFieldOfSubMessage() {
+ // Ignore o_int of sub message fields.
+ Message message = parse("o_int: 1 o_sub_test_message: { o_int: 2 r_string: \"foo\" }");
+ Message diffMessage1 = parse("o_int: 2 o_sub_test_message: { o_int: 2 r_string: \"foo\" }");
+ Message diffMessage2 = parse("o_int: 1 o_sub_test_message: { o_int: 2 r_string: \"bar\" }");
+ Message eqMessage = parse("o_int: 1 o_sub_test_message: { o_int: 3 r_string: \"foo\" }");
+
+ FieldDescriptor fieldDescriptor =
+ getFieldDescriptor("o_sub_test_message").getMessageType().findFieldByName("o_int");
+ FieldScope partialScope = FieldScopes.ignoringFieldDescriptors(fieldDescriptor);
+
+ expectThat(diffMessage1).withPartialScope(partialScope).isNotEqualTo(message);
+ expectThat(diffMessage2).withPartialScope(partialScope).isNotEqualTo(message);
+ expectThat(eqMessage).withPartialScope(partialScope).isEqualTo(message);
+
+ expectFailureWhenTesting().that(diffMessage1).withPartialScope(partialScope).isEqualTo(message);
+ expectIsEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("modified: o_int: 1 -> 2");
+
+ expectFailureWhenTesting().that(diffMessage2).withPartialScope(partialScope).isEqualTo(message);
+ expectIsEqualToFailed();
+ expectThatFailure()
+ .hasMessageThat()
+ .contains("modified: o_sub_test_message.r_string[0]: \"foo\" -> \"bar\"");
+ }
+
+ @Test
+ public void testIgnoringFieldOfAnyMessage() throws Exception {
+ String typeUrl =
+ isProto3()
+ ? "type.googleapis.com/com.google.common.truth.extensions.proto.SubTestMessage3"
+ : "type.googleapis.com/com.google.common.truth.extensions.proto.SubTestMessage2";
+
+ Message message =
+ parse("o_int: 1 o_any_message { [" + typeUrl + "]: { o_int: 2 r_string: \"foo\" } }");
+ Message diffMessage1 =
+ parse("o_int: 2 o_any_message { [" + typeUrl + "]: { o_int: 2 r_string: \"foo\" } }");
+ Message diffMessage2 =
+ parse("o_int: 1 o_any_message { [" + typeUrl + "]: { o_int: 2 r_string: \"bar\" } }");
+ Message eqMessage =
+ parse("o_int: 1 o_any_message { [" + typeUrl + "]: { o_int: 3 r_string: \"foo\" } }");
+
+ FieldDescriptor fieldDescriptor =
+ getTypeRegistry().getDescriptorForTypeUrl(typeUrl).findFieldByName("o_int");
+ FieldScope partialScope = FieldScopes.ignoringFieldDescriptors(fieldDescriptor);
+ expectThat(diffMessage1)
+ .unpackingAnyUsing(getTypeRegistry(), getExtensionRegistry())
+ .withPartialScope(partialScope)
+ .isNotEqualTo(message);
+ expectThat(diffMessage2)
+ .unpackingAnyUsing(getTypeRegistry(), getExtensionRegistry())
+ .withPartialScope(partialScope)
+ .isNotEqualTo(message);
+ expectThat(eqMessage)
+ .unpackingAnyUsing(getTypeRegistry(), getExtensionRegistry())
+ .withPartialScope(partialScope)
+ .isEqualTo(message);
+
+ expectFailureWhenTesting()
+ .that(diffMessage1)
+ .unpackingAnyUsing(getTypeRegistry(), getExtensionRegistry())
+ .withPartialScope(partialScope)
+ .isEqualTo(message);
+ expectIsEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("modified: o_int: 1 -> 2");
+
+ expectFailureWhenTesting()
+ .that(diffMessage2)
+ .unpackingAnyUsing(getTypeRegistry(), getExtensionRegistry())
+ .withPartialScope(partialScope)
+ .isEqualTo(message);
+ expectIsEqualToFailed();
+ expectThatFailure()
+ .hasMessageThat()
+ .contains("modified: o_any_message.value.r_string[0]: \"foo\" -> \"bar\"");
+ }
+
+ @Test
+ public void testIgnoringAllButOneFieldOfSubMessage() {
+ // Consider all of TestMessage, but none of o_sub_test_message, except
+ // o_sub_test_message.o_int.
+ Message message =
+ parse(
+ "o_int: 3 o_sub_test_message: { o_int: 4 r_string: \"foo\" } "
+ + "r_sub_test_message: { o_int: 5 r_string: \"bar\" }");
+
+ // All of these differ in a critical field.
+ Message diffMessage1 =
+ parse(
+ "o_int: 999999 o_sub_test_message: { o_int: 4 r_string: \"foo\" } "
+ + "r_sub_test_message: { o_int: 5 r_string: \"bar\" }");
+ Message diffMessage2 =
+ parse(
+ "o_int: 3 o_sub_test_message: { o_int: 999999 r_string: \"foo\" } "
+ + "r_sub_test_message: { o_int: 5 r_string: \"bar\" }");
+ Message diffMessage3 =
+ parse(
+ "o_int: 3 o_sub_test_message: { o_int: 4 r_string: \"foo\" } "
+ + "r_sub_test_message: { o_int: 999999 r_string: \"bar\" }");
+ Message diffMessage4 =
+ parse(
+ "o_int: 3 o_sub_test_message: { o_int: 4 r_string: \"foo\" } "
+ + "r_sub_test_message: { o_int: 5 r_string: \"999999\" }");
+
+ // This one only differs in o_sub_test_message.r_string, which is ignored.
+ Message eqMessage =
+ parse(
+ "o_int: 3 o_sub_test_message: { o_int: 4 r_string: \"999999\" } "
+ + "r_sub_test_message: { o_int: 5 r_string: \"bar\" }");
+
+ FieldScope fieldScope =
+ FieldScopes.ignoringFields(getFieldNumber("o_sub_test_message"))
+ .allowingFieldDescriptors(
+ getFieldDescriptor("o_sub_test_message").getMessageType().findFieldByName("o_int"));
+
+ expectThat(diffMessage1).withPartialScope(fieldScope).isNotEqualTo(message);
+ expectThat(diffMessage2).withPartialScope(fieldScope).isNotEqualTo(message);
+ expectThat(diffMessage3).withPartialScope(fieldScope).isNotEqualTo(message);
+ expectThat(diffMessage4).withPartialScope(fieldScope).isNotEqualTo(message);
+ expectThat(eqMessage).withPartialScope(fieldScope).isEqualTo(message);
+
+ expectFailureWhenTesting().that(diffMessage4).withPartialScope(fieldScope).isEqualTo(message);
+ expectIsEqualToFailed();
+ expectThatFailure()
+ .hasMessageThat()
+ .contains("modified: r_sub_test_message[0].r_string[0]: \"bar\" -> \"999999\"");
+
+ expectFailureWhenTesting().that(eqMessage).withPartialScope(fieldScope).isNotEqualTo(message);
+ expectIsNotEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("ignored: o_sub_test_message.r_string");
+ }
+
+ @Test
+ public void testFromSetFields() {
+ Message scopeMessage =
+ parse(
+ "o_int: 1 r_string: \"x\" o_test_message: { o_int: 1 } "
+ + "r_test_message: { r_string: \"x\" } r_test_message: { o_int: 1 } "
+ + "o_sub_test_message: { o_test_message: { o_int: 1 } }");
+
+ // 1 = compared, [2, 3] = ignored, 4 = compared and fails
+ Message message =
+ parse(
+ "o_int: 1 r_string: \"1\" o_test_message: {o_int: 1 r_string: \"2\" } "
+ + "r_test_message: { o_int: 1 r_string: \"1\" } "
+ + "r_test_message: { o_int: 1 r_string: \"1\" } "
+ + "o_sub_test_message: { o_int: 2 o_test_message: { o_int: 1 r_string: \"2\" } }");
+ Message diffMessage =
+ parse(
+ "o_int: 4 r_string: \"4\" o_test_message: {o_int: 4 r_string: \"3\" } "
+ + "r_test_message: { o_int: 4 r_string: \"4\" } "
+ + "r_test_message: { o_int: 4 r_string: \"4\" }"
+ + "o_sub_test_message: { r_string: \"3\" o_int: 3 "
+ + "o_test_message: { o_int: 4 r_string: \"3\" } }");
+ Message eqMessage =
+ parse(
+ "o_int: 1 r_string: \"1\" o_test_message: {o_int: 1 r_string: \"3\" } "
+ + "r_test_message: { o_int: 1 r_string: \"1\" } "
+ + "r_test_message: { o_int: 1 r_string: \"1\" }"
+ + "o_sub_test_message: { o_int: 3 o_test_message: { o_int: 1 r_string: \"3\" } }");
+
+ expectThat(diffMessage).isNotEqualTo(message);
+ expectThat(eqMessage).isNotEqualTo(message);
+
+ expectThat(diffMessage)
+ .withPartialScope(FieldScopes.fromSetFields(scopeMessage))
+ .isNotEqualTo(message);
+ expectThat(eqMessage)
+ .withPartialScope(FieldScopes.fromSetFields(scopeMessage))
+ .isEqualTo(message);
+
+ expectFailureWhenTesting().that(diffMessage).isEqualTo(message);
+ expectIsEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("1 -> 4");
+ expectThatFailure().hasMessageThat().contains("\"1\" -> \"4\"");
+ expectThatFailure().hasMessageThat().contains("2 -> 3");
+ expectThatFailure().hasMessageThat().contains("\"2\" -> \"3\"");
+
+ expectFailureWhenTesting()
+ .that(diffMessage)
+ .withPartialScope(FieldScopes.fromSetFields(scopeMessage))
+ .isEqualTo(message);
+ expectIsEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("1 -> 4");
+ expectThatFailure().hasMessageThat().contains("\"1\" -> \"4\"");
+ expectThatFailure().hasMessageThat().doesNotContain("2 -> 3");
+ expectThatFailure().hasMessageThat().doesNotContain("\"2\" -> \"3\"");
+
+ expectFailureWhenTesting()
+ .that(eqMessage)
+ .withPartialScope(FieldScopes.fromSetFields(scopeMessage))
+ .isNotEqualTo(message);
+ expectIsNotEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("ignored: o_test_message.r_string");
+ expectThatFailure().hasMessageThat().contains("ignored: o_sub_test_message.o_int");
+ expectThatFailure()
+ .hasMessageThat()
+ .contains("ignored: o_sub_test_message.o_test_message.r_string");
+ }
+
+ @Test
+ public void testFromSetFields_comparingExpectedFieldsOnly()
+ throws InvalidProtocolBufferException {
+
+ Message message1 = parse("o_int: 1 o_double: 333 oneof_message1: { o_int: 3 o_double: 333 }");
+ Message message2 =
+ parse("o_int: 333 o_double: 1.2 oneof_message2: { o_int: 333 o_double: 3.14 }");
+ Message diffMessage1 = parse("o_int: 1 oneof_message1: { o_int: 4 }");
+ Message diffMessage2 = parse("o_double: 1.2 oneof_message2: { o_double: 4.14 }");
+ Message eqMessage1 = parse("o_int: 1 oneof_message1: { o_int: 3 }");
+ Message eqMessage2 = parse("o_double: 1.2 oneof_message2: { o_double: 3.14 }");
+
+ expectThat(message1).comparingExpectedFieldsOnly().isEqualTo(eqMessage1);
+ expectThat(message2).comparingExpectedFieldsOnly().isEqualTo(eqMessage2);
+ expectFailureWhenTesting().that(message1).comparingExpectedFieldsOnly().isEqualTo(diffMessage1);
+ expectFailureWhenTesting().that(message2).comparingExpectedFieldsOnly().isEqualTo(diffMessage2);
+
+ expectThat(listOf(message1, message2))
+ .comparingExpectedFieldsOnly()
+ .containsExactly(eqMessage1, eqMessage2);
+ expectFailureWhenTesting()
+ .that(listOf(message1, message2))
+ .comparingExpectedFieldsOnly()
+ .containsExactly(diffMessage1, eqMessage2);
+ expectFailureWhenTesting()
+ .that(listOf(message1, message2))
+ .comparingExpectedFieldsOnly()
+ .containsExactly(eqMessage1, diffMessage2);
+ }
+
+ @Test
+ public void testFromSetFields_unknownFields() throws InvalidProtocolBufferException {
+ // Make sure that merging of repeated fields, separation by tag number, and separation by
+ // unknown field type all work.
+ Message scopeMessage =
+ fromUnknownFields(
+ UnknownFieldSet.newBuilder()
+ .addField(333, Field.newBuilder().addFixed32(1).addFixed64(1).build())
+ .addField(
+ 444,
+ Field.newBuilder()
+ .addVarint(1)
+ .addLengthDelimited(ByteString.copyFrom("1", UTF_8))
+ .addGroup(
+ UnknownFieldSet.newBuilder()
+ .addField(1, Field.newBuilder().addFixed32(1).build())
+ .build())
+ .addGroup(
+ UnknownFieldSet.newBuilder()
+ .addField(2, Field.newBuilder().addFixed64(1).build())
+ .build())
+ .build())
+ .build());
+
+ // 1 = compared, [2, 3] = ignored, 4 = compared and fails
+ Message message =
+ fromUnknownFields(
+ UnknownFieldSet.newBuilder()
+ .addField(222, Field.newBuilder().addFixed32(2).addFixed64(2).build())
+ .addField(
+ 333,
+ Field.newBuilder()
+ .addFixed32(1)
+ .addFixed64(1)
+ .addVarint(2)
+ .addLengthDelimited(ByteString.copyFrom("2", UTF_8))
+ .addGroup(
+ UnknownFieldSet.newBuilder()
+ .addField(1, Field.newBuilder().addFixed32(2).build())
+ .build())
+ .build())
+ .addField(
+ 444,
+ Field.newBuilder()
+ .addFixed32(2)
+ .addFixed64(2)
+ .addVarint(1)
+ .addLengthDelimited(ByteString.copyFrom("1", UTF_8))
+ .addGroup(
+ UnknownFieldSet.newBuilder()
+ .addField(1, Field.newBuilder().addFixed32(1).addFixed64(2).build())
+ .addField(2, Field.newBuilder().addFixed32(2).addFixed64(1).build())
+ .addField(3, Field.newBuilder().addFixed32(2).build())
+ .build())
+ .build())
+ .build());
+ Message diffMessage =
+ fromUnknownFields(
+ UnknownFieldSet.newBuilder()
+ .addField(222, Field.newBuilder().addFixed32(3).addFixed64(3).build())
+ .addField(
+ 333,
+ Field.newBuilder()
+ .addFixed32(4)
+ .addFixed64(4)
+ .addVarint(3)
+ .addLengthDelimited(ByteString.copyFrom("3", UTF_8))
+ .addGroup(
+ UnknownFieldSet.newBuilder()
+ .addField(1, Field.newBuilder().addFixed32(3).build())
+ .build())
+ .build())
+ .addField(
+ 444,
+ Field.newBuilder()
+ .addFixed32(3)
+ .addFixed64(3)
+ .addVarint(4)
+ .addLengthDelimited(ByteString.copyFrom("4", UTF_8))
+ .addGroup(
+ UnknownFieldSet.newBuilder()
+ .addField(1, Field.newBuilder().addFixed32(4).addFixed64(3).build())
+ .addField(2, Field.newBuilder().addFixed32(3).addFixed64(4).build())
+ .addField(3, Field.newBuilder().addFixed32(3).build())
+ .build())
+ .build())
+ .build());
+ Message eqMessage =
+ fromUnknownFields(
+ UnknownFieldSet.newBuilder()
+ .addField(222, Field.newBuilder().addFixed32(3).addFixed64(3).build())
+ .addField(
+ 333,
+ Field.newBuilder()
+ .addFixed32(1)
+ .addFixed64(1)
+ .addVarint(3)
+ .addLengthDelimited(ByteString.copyFrom("3", UTF_8))
+ .addGroup(
+ UnknownFieldSet.newBuilder()
+ .addField(1, Field.newBuilder().addFixed32(3).build())
+ .build())
+ .build())
+ .addField(
+ 444,
+ Field.newBuilder()
+ .addFixed32(3)
+ .addFixed64(3)
+ .addVarint(1)
+ .addLengthDelimited(ByteString.copyFrom("1", UTF_8))
+ .addGroup(
+ UnknownFieldSet.newBuilder()
+ .addField(1, Field.newBuilder().addFixed32(1).addFixed64(3).build())
+ .addField(2, Field.newBuilder().addFixed32(3).addFixed64(1).build())
+ .addField(3, Field.newBuilder().addFixed32(3).build())
+ .build())
+ .build())
+ .build());
+
+ expectThat(diffMessage).isNotEqualTo(message);
+ expectThat(eqMessage).isNotEqualTo(message);
+
+ expectThat(diffMessage)
+ .withPartialScope(FieldScopes.fromSetFields(scopeMessage))
+ .isNotEqualTo(message);
+ expectThat(eqMessage)
+ .withPartialScope(FieldScopes.fromSetFields(scopeMessage))
+ .isEqualTo(message);
+
+ expectFailureWhenTesting().that(diffMessage).isEqualTo(message);
+ expectIsEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("1 -> 4");
+ expectThatFailure().hasMessageThat().contains("\"1\" -> \"4\"");
+ expectThatFailure().hasMessageThat().contains("2 -> 3");
+ expectThatFailure().hasMessageThat().contains("\"2\" -> \"3\"");
+
+ expectFailureWhenTesting()
+ .that(diffMessage)
+ .withPartialScope(FieldScopes.fromSetFields(scopeMessage))
+ .isEqualTo(message);
+ expectIsEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("1 -> 4");
+ expectThatFailure().hasMessageThat().contains("\"1\" -> \"4\"");
+ expectThatFailure().hasMessageThat().doesNotContain("2 -> 3");
+ expectThatFailure().hasMessageThat().doesNotContain("\"2\" -> \"3\"");
+
+ expectFailureWhenTesting()
+ .that(eqMessage)
+ .withPartialScope(FieldScopes.fromSetFields(scopeMessage))
+ .isNotEqualTo(message);
+ expectIsNotEqualToFailed();
+ expectThatFailure().hasMessageThat().doesNotContain("2 -> 3");
+ expectThatFailure().hasMessageThat().doesNotContain("\"2\" -> \"3\"");
+ }
+
+ @Test
+ public void testFieldNumbersAreRecursive() {
+ // o_int is compared, r_string is not.
+ Message message =
+ parse("o_int: 1 r_string: \"foo\" r_test_message: { o_int: 2 r_string: \"bar\" }");
+ Message diffMessage =
+ parse("o_int: 2 r_string: \"bar\" r_test_message: { o_int: 1 r_string: \"foo\" }");
+ Message eqMessage =
+ parse("o_int: 1 r_string: \"bar\" r_test_message: { o_int: 2 r_string: \"foo\" }");
+ int fieldNumber = getFieldNumber("o_int");
+ FieldDescriptor fieldDescriptor = getFieldDescriptor("o_int");
+
+ expectThat(diffMessage)
+ .withPartialScope(FieldScopes.allowingFields(fieldNumber))
+ .isNotEqualTo(message);
+ expectThat(eqMessage)
+ .withPartialScope(FieldScopes.allowingFields(fieldNumber))
+ .isEqualTo(message);
+ expectThat(diffMessage)
+ .withPartialScope(FieldScopes.allowingFieldDescriptors(fieldDescriptor))
+ .isNotEqualTo(message);
+ expectThat(eqMessage)
+ .withPartialScope(FieldScopes.allowingFieldDescriptors(fieldDescriptor))
+ .isEqualTo(message);
+
+ expectFailureWhenTesting()
+ .that(diffMessage)
+ .withPartialScope(FieldScopes.allowingFields(fieldNumber))
+ .isEqualTo(message);
+ expectIsEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("modified: o_int: 1 -> 2");
+ expectThatFailure().hasMessageThat().contains("modified: r_test_message[0].o_int: 2 -> 1");
+
+ expectFailureWhenTesting()
+ .that(eqMessage)
+ .withPartialScope(FieldScopes.allowingFields(fieldNumber))
+ .isNotEqualTo(message);
+ expectIsNotEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("ignored: r_test_message[0].r_string");
+ }
+
+ @Test
+ public void testMultipleFieldNumbers() {
+ Message message = parse("o_int: 1 r_string: \"x\" o_enum: TWO");
+ Message diffMessage = parse("o_int: 2 r_string: \"y\" o_enum: TWO");
+ Message eqMessage =
+ parse("o_int: 1 r_string: \"x\" o_enum: ONE o_sub_test_message: { r_string: \"bar\" }");
+
+ FieldScope fieldScope =
+ FieldScopes.allowingFields(getFieldNumber("o_int"), getFieldNumber("r_string"));
+
+ expectThat(diffMessage).withPartialScope(fieldScope).isNotEqualTo(message);
+ expectThat(eqMessage).withPartialScope(fieldScope).isEqualTo(message);
+
+ expectFailureWhenTesting().that(diffMessage).withPartialScope(fieldScope).isEqualTo(message);
+ expectIsEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("modified: o_int: 1 -> 2");
+ expectThatFailure().hasMessageThat().contains("modified: r_string[0]: \"x\" -> \"y\"");
+
+ expectFailureWhenTesting().that(eqMessage).withPartialScope(fieldScope).isNotEqualTo(message);
+ expectIsNotEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("ignored: o_enum");
+ expectThatFailure().hasMessageThat().contains("ignored: o_sub_test_message");
+ }
+
+ @Test
+ public void testInvalidFieldNumber() {
+ Message message1 = parse("o_int: 44");
+ Message message2 = parse("o_int: 33");
+
+ try {
+ assertThat(message1).ignoringFields(999).isEqualTo(message2);
+ fail("Expected failure.");
+ } catch (Exception expected) {
+ // TODO(user): Use hasTransitiveCauseThat() if/when it becomes available.
+
+ Throwable cause = expected;
+ while (cause != null) {
+ if (cause
+ .getMessage()
+ .contains("Message type " + fullMessageName() + " has no field with number 999.")) {
+ break;
+ } else {
+ cause = cause.getCause();
+ }
+ }
+ if (cause == null) {
+ fail("No cause with field number error message.");
+ }
+ }
+ }
+
+ @Test
+ public void testIgnoreFieldsAtDifferentLevels() {
+ // Ignore all 'o_int' fields, in different ways.
+ Message message =
+ parse(
+ "o_int: 1 r_string: \"foo\" o_sub_test_message: { o_int: 2 "
+ + "o_sub_sub_test_message: { o_int: 3 r_string: \"bar\" } }");
+
+ // Even though o_int is ignored, message presence is not. So these all fail.
+ Message diffMessage1 = parse("r_string: \"baz\"");
+ Message diffMessage2 = parse("r_string: \"foo\"");
+ Message diffMessage3 = parse("r_string: \"foo\" o_sub_test_message: {}");
+ Message diffMessage4 =
+ parse("r_string: \"foo\" o_sub_test_message: { o_sub_sub_test_message: {} }");
+
+ // All of these messages are equivalent, because all o_int are ignored.
+ Message eqMessage1 =
+ parse(
+ "o_int: 111 r_string: \"foo\" o_sub_test_message: { o_int: 222 "
+ + "o_sub_sub_test_message: { o_int: 333 r_string: \"bar\" } }");
+ Message eqMessage2 =
+ parse(
+ "o_int: 1 r_string: \"foo\" o_sub_test_message: { o_int: 2 "
+ + "o_sub_sub_test_message: { o_int: 3 r_string: \"bar\" } }");
+ Message eqMessage3 =
+ parse(
+ "r_string: \"foo\" o_sub_test_message: { "
+ + "o_sub_sub_test_message: { r_string: \"bar\" } }");
+ Message eqMessage4 =
+ parse(
+ "o_int: 333 r_string: \"foo\" o_sub_test_message: { o_int: 111 "
+ + "o_sub_sub_test_message: { o_int: 222 r_string: \"bar\" } }");
+
+ FieldDescriptor top = getFieldDescriptor("o_int");
+ FieldDescriptor middle =
+ getFieldDescriptor("o_sub_test_message").getMessageType().findFieldByName("o_int");
+ FieldDescriptor bottom =
+ getFieldDescriptor("o_sub_test_message")
+ .getMessageType()
+ .findFieldByName("o_sub_sub_test_message")
+ .getMessageType()
+ .findFieldByName("o_int");
+
+ ImmutableMap<String, FieldScope> fieldScopes =
+ ImmutableMap.of(
+ "BASIC", FieldScopes.ignoringFieldDescriptors(top, middle, bottom),
+ "CHAINED",
+ FieldScopes.ignoringFieldDescriptors(top)
+ .ignoringFieldDescriptors(middle)
+ .ignoringFieldDescriptors(bottom),
+ "REPEATED",
+ FieldScopes.ignoringFieldDescriptors(top, middle)
+ .ignoringFieldDescriptors(middle, bottom));
+
+ for (String scopeName : fieldScopes.keySet()) {
+ String msg = "FieldScope(" + scopeName + ")";
+ FieldScope scope = fieldScopes.get(scopeName);
+
+ expectThatWithMessage(msg, diffMessage1).withPartialScope(scope).isNotEqualTo(message);
+ expectThatWithMessage(msg, diffMessage2).withPartialScope(scope).isNotEqualTo(message);
+ expectThatWithMessage(msg, diffMessage3).withPartialScope(scope).isNotEqualTo(message);
+ expectThatWithMessage(msg, diffMessage4).withPartialScope(scope).isNotEqualTo(message);
+
+ expectThatWithMessage(msg, eqMessage1).withPartialScope(scope).isEqualTo(message);
+ expectThatWithMessage(msg, eqMessage2).withPartialScope(scope).isEqualTo(message);
+ expectThatWithMessage(msg, eqMessage3).withPartialScope(scope).isEqualTo(message);
+ expectThatWithMessage(msg, eqMessage4).withPartialScope(scope).isEqualTo(message);
+ }
+ }
+
+ @Test
+ public void testFromSetFields_skipNulls() {
+ Message message1 = parse("o_int: 1 r_string: \"foo\" r_string: \"bar\"");
+ Message eqMessage1 = parse("o_int: 1 r_string: \"foo\" r_string: \"bar\"");
+ Message eqIgnoredMessage1 = parse("o_int: 2 r_string: \"foo\" r_string: \"bar\"");
+ Message message2 = parse("o_int: 3 r_string: \"baz\" r_string: \"qux\"");
+ Message eqMessage2 = parse("o_int: 3 r_string: \"baz\" r_string: \"qux\"");
+ Message eqIgnoredMessage2 = parse("o_int: 4 r_string: \"baz\" r_string: \"qux\"");
+
+ List<Message> messages = Lists.newArrayList();
+ Message nullMessage = null;
+ messages.add(parse("o_int: -1"));
+ messages.add(nullMessage);
+ messages.add(parse("r_string: \"NaN\""));
+
+ expectThat(listOf(message1, message2))
+ .withPartialScope(FieldScopes.fromSetFields(messages))
+ .containsExactly(eqMessage1, eqMessage2);
+ expectThat(listOf(message1, message2))
+ .withPartialScope(
+ FieldScopes.fromSetFields(parse("o_int: -1"), nullMessage, parse("r_string: \"NaN\"")))
+ .containsExactly(eqMessage1, eqMessage2);
+
+ expectFailureWhenTesting()
+ .that(listOf(message1, message2))
+ .withPartialScope(FieldScopes.fromSetFields(messages))
+ .containsExactly(eqIgnoredMessage1, eqIgnoredMessage2);
+ expectThatFailure()
+ .factValue("testing whether")
+ .contains(
+ "is equivalent according to "
+ + "assertThat(proto)"
+ + ".withPartialScope("
+ + "FieldScopes.fromSetFields(["
+ + "{o_int: -1\n}, null, {r_string: \"NaN\"\n}]))"
+ + ".isEqualTo(target)");
+
+ expectFailureWhenTesting()
+ .that(listOf(message1, message2))
+ .withPartialScope(
+ FieldScopes.fromSetFields(parse("o_int: -1"), nullMessage, parse("r_string: \"NaN\"")))
+ .containsExactly(eqIgnoredMessage1, eqIgnoredMessage2);
+ expectThatFailure()
+ .factValue("testing whether")
+ .contains(
+ "is equivalent according to "
+ + "assertThat(proto)"
+ + ".withPartialScope("
+ + "FieldScopes.fromSetFields(["
+ + "{o_int: -1\n}, null, {r_string: \"NaN\"\n}]))"
+ + ".isEqualTo(target)");
+ }
+
+ @Test
+ public void testFromSetFields_iterables_vacuousIfEmptyOrAllNull() {
+ Message message1 = parse("o_int: 1 r_string: \"foo\" r_string: \"bar\"");
+ Message eqIgnoredMessage1 = parse("o_int: 2 r_string: \"foo\" r_string: \"bar\"");
+ Message message2 = parse("o_int: 3 r_string: \"baz\" r_string: \"qux\"");
+ Message eqIgnoredMessage2 = parse("o_int: 4 r_string: \"baz\" r_string: \"qux\"");
+
+ List<Message> messages = Lists.newArrayList();
+ messages.add(null);
+ messages.add(null);
+
+ expectThat(listOf(message1, message2))
+ .withPartialScope(FieldScopes.fromSetFields(ImmutableList.<Message>of()))
+ .containsExactly(eqIgnoredMessage1, eqIgnoredMessage2);
+ expectThat(listOf(message1, message2))
+ .withPartialScope(FieldScopes.fromSetFields(messages))
+ .containsExactly(eqIgnoredMessage1, eqIgnoredMessage2);
+
+ expectFailureWhenTesting()
+ .that(listOf(message1, message2))
+ .withPartialScope(FieldScopes.fromSetFields(ImmutableList.<Message>of()))
+ .containsNoneOf(eqIgnoredMessage1, eqIgnoredMessage2);
+
+ expectFailureWhenTesting()
+ .that(listOf(message1, message2))
+ .withPartialScope(FieldScopes.fromSetFields(messages))
+ .containsNoneOf(eqIgnoredMessage1, eqIgnoredMessage2);
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testFromSetFields_iterables_errorForDifferentMessageTypes() {
+ // Don't run this test twice.
+ if (!testIsRunOnce()) {
+ return;
+ }
+
+ try {
+ FieldScopes.fromSetFields(
+ TestMessage2.newBuilder().setOInt(2).build(),
+ TestMessage3.newBuilder().setOInt(2).build());
+ fail("Expected failure.");
+ } catch (RuntimeException expected) {
+ expect
+ .that(expected)
+ .hasMessageThat()
+ .contains("Cannot create scope from messages with different descriptors");
+ expect.that(expected).hasMessageThat().contains(TestMessage2.getDescriptor().getFullName());
+ expect.that(expected).hasMessageThat().contains(TestMessage3.getDescriptor().getFullName());
+ }
+ }
+
+ @Test
+ public void testFromSetFields_iterables_errorIfDescriptorMismatchesSubject() {
+ // Don't run this test twice.
+ if (!testIsRunOnce()) {
+ return;
+ }
+
+ Message message =
+ TestMessage2.newBuilder().setOInt(1).addRString("foo").addRString("bar").build();
+ Message eqMessage =
+ TestMessage2.newBuilder().setOInt(1).addRString("foo").addRString("bar").build();
+
+ try {
+ assertThat(message)
+ .withPartialScope(
+ FieldScopes.fromSetFields(
+ TestMessage3.newBuilder().setOInt(2).build(),
+ TestMessage3.newBuilder().addRString("foo").build()))
+ .isEqualTo(eqMessage);
+ fail("Expected failure.");
+ } catch (RuntimeException expected) {
+ expect
+ .that(expected)
+ .hasMessageThat()
+ .contains(
+ "Message given to FieldScopes.fromSetFields() "
+ + "does not have the same descriptor as the message being tested");
+ expect.that(expected).hasMessageThat().contains(TestMessage2.getDescriptor().getFullName());
+ expect.that(expected).hasMessageThat().contains(TestMessage3.getDescriptor().getFullName());
+ }
+ }
+
+ @Test
+ public void testFromSetFields_iterables_unionsElements() {
+ Message message = parse("o_int: 1 r_string: \"foo\" r_string: \"bar\"");
+ Message diffMessage1 = parse("o_int: 2 r_string: \"foo\" r_string: \"bar\"");
+ Message diffMessage2 = parse("o_int: 4 r_string: \"baz\" r_string: \"qux\"");
+
+ expectThat(listOf(message))
+ .ignoringFieldScope(FieldScopes.fromSetFields(parse("o_int: 1"), parse("o_enum: TWO")))
+ .containsExactly(diffMessage1);
+
+ expectFailureWhenTesting()
+ .that(listOf(message))
+ .ignoringFieldScope(FieldScopes.fromSetFields(parse("o_int: 1"), parse("o_enum: TWO")))
+ .containsExactly(diffMessage2);
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testIterableFieldScopeMethodVariants_protoSubject() {
+ Message message = parse("o_int: 1 r_string: \"foo\"");
+ Message eqExceptInt = parse("o_int: 2 r_string: \"foo\"");
+
+ expectThat(message).ignoringFields(listOf(getFieldNumber("o_int"))).isEqualTo(eqExceptInt);
+ expectThat(message)
+ .reportingMismatchesOnly()
+ .ignoringFields(listOf(getFieldNumber("o_int")))
+ .isEqualTo(eqExceptInt);
+ expectThat(message)
+ .ignoringFieldScope(FieldScopes.allowingFields(listOf(getFieldNumber("o_int"))))
+ .isEqualTo(eqExceptInt);
+ expectThat(message)
+ .withPartialScope(FieldScopes.ignoringFields(listOf(getFieldNumber("o_int"))))
+ .isEqualTo(eqExceptInt);
+ expectThat(message)
+ .ignoringFieldDescriptors(listOf(getFieldDescriptor("o_int")))
+ .isEqualTo(eqExceptInt);
+ expectThat(message)
+ .reportingMismatchesOnly()
+ .ignoringFieldDescriptors(listOf(getFieldDescriptor("o_int")))
+ .isEqualTo(eqExceptInt);
+ expectThat(message)
+ .ignoringFieldScope(
+ FieldScopes.allowingFieldDescriptors(listOf(getFieldDescriptor("o_int"))))
+ .isEqualTo(eqExceptInt);
+ expectThat(message)
+ .withPartialScope(FieldScopes.ignoringFieldDescriptors(listOf(getFieldDescriptor("o_int"))))
+ .isEqualTo(eqExceptInt);
+ }
+
+ @Test
+ public void testIterableFieldScopeMethodVariants_iterableOfProtosSubject() {
+ ImmutableList<Message> messages = listOf(parse("o_int: 1 r_string: \"foo\""));
+ ImmutableList<Message> eqExceptInt = listOf(parse("o_int: 2 r_string: \"foo\""));
+
+ expectThat(messages)
+ .ignoringFields(listOf(getFieldNumber("o_int")))
+ .containsExactlyElementsIn(eqExceptInt);
+ expectThat(messages)
+ .reportingMismatchesOnly()
+ .ignoringFields(listOf(getFieldNumber("o_int")))
+ .containsExactlyElementsIn(eqExceptInt);
+ expectThat(messages)
+ .ignoringFieldDescriptors(listOf(getFieldDescriptor("o_int")))
+ .containsExactlyElementsIn(eqExceptInt);
+ expectThat(messages)
+ .reportingMismatchesOnly()
+ .ignoringFieldDescriptors(listOf(getFieldDescriptor("o_int")))
+ .containsExactlyElementsIn(eqExceptInt);
+ }
+
+ @Test
+ public void testIterableFieldScopeMethodVariants_mapWithProtoValuesSubject() {
+ ImmutableMap<String, Message> messages =
+ ImmutableMap.of("foo", parse("o_int: 1 r_string: \"foo\""));
+ ImmutableMap<String, Message> eqExceptInt =
+ ImmutableMap.of("foo", parse("o_int: 2 r_string: \"foo\""));
+
+ expectThat(messages)
+ .ignoringFieldsForValues(listOf(getFieldNumber("o_int")))
+ .containsExactlyEntriesIn(eqExceptInt);
+ expectThat(messages)
+ .reportingMismatchesOnlyForValues()
+ .ignoringFieldsForValues(listOf(getFieldNumber("o_int")))
+ .containsExactlyEntriesIn(eqExceptInt);
+ expectThat(messages)
+ .ignoringFieldDescriptorsForValues(listOf(getFieldDescriptor("o_int")))
+ .containsExactlyEntriesIn(eqExceptInt);
+ expectThat(messages)
+ .reportingMismatchesOnlyForValues()
+ .ignoringFieldDescriptorsForValues(listOf(getFieldDescriptor("o_int")))
+ .containsExactlyEntriesIn(eqExceptInt);
+ }
+
+ @Test
+ public void testIterableFieldScopeMethodVariants_multimapWithProtoValuesSubject() {
+ ImmutableMultimap<String, Message> messages =
+ ImmutableMultimap.of("foo", parse("o_int: 1 r_string: \"foo\""));
+ ImmutableMultimap<String, Message> eqExceptInt =
+ ImmutableMultimap.of("foo", parse("o_int: 2 r_string: \"foo\""));
+
+ expectThat(messages)
+ .ignoringFieldsForValues(listOf(getFieldNumber("o_int")))
+ .containsExactlyEntriesIn(eqExceptInt);
+ expectThat(messages)
+ .reportingMismatchesOnlyForValues()
+ .ignoringFieldsForValues(listOf(getFieldNumber("o_int")))
+ .containsExactlyEntriesIn(eqExceptInt);
+ expectThat(messages)
+ .ignoringFieldDescriptorsForValues(listOf(getFieldDescriptor("o_int")))
+ .containsExactlyEntriesIn(eqExceptInt);
+ expectThat(messages)
+ .reportingMismatchesOnlyForValues()
+ .ignoringFieldDescriptorsForValues(listOf(getFieldDescriptor("o_int")))
+ .containsExactlyEntriesIn(eqExceptInt);
+ }
+}
diff --git a/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/IterableOfProtosSubjectTest.java b/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/IterableOfProtosSubjectTest.java
new file mode 100644
index 00000000..7a06845e
--- /dev/null
+++ b/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/IterableOfProtosSubjectTest.java
@@ -0,0 +1,532 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.Message;
+import java.util.Collection;
+import java.util.Comparator;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Tests for {@link IterableOfProtosSubject}.
+ *
+ * <p>Individual equality fuzzing is thoroughly tested by {@link ProtoSubjectTest}, while fuzzy
+ * equality testing is thoroughly tested by {@link com.google.common.truth.IterableSubjectTest}.
+ * Thus, we simply check that all of the exposed methods work in basic cases, and trust that the
+ * implementation ensures correctness in the cross-product of the many ways one can do things.
+ */
+@RunWith(Parameterized.class)
+public class IterableOfProtosSubjectTest extends ProtoSubjectTestBase {
+
+ private final Message message1 = parse("o_int: 1 r_string: \"foo\" r_string: \"bar\"");
+ private final Message eqMessage1 = parse("o_int: 1 r_string: \"foo\" r_string: \"bar\"");
+ private final Message eqRepeatedMessage1 = parse("o_int: 1 r_string: \"bar\" r_string: \"foo\"");
+ private final Message eqIgnoredMessage1 = parse("o_int: 2 r_string: \"foo\" r_string: \"bar\"");
+ private final Message message2 = parse("o_int: 3 r_string: \"baz\" r_string: \"qux\"");
+ private final Message eqMessage2 = parse("o_int: 3 r_string: \"baz\" r_string: \"qux\"");
+ private final Message eqRepeatedMessage2 = parse("o_int: 3 r_string: \"qux\" r_string: \"baz\"");
+ private final Message eqIgnoredMessage2 = parse("o_int: 4 r_string: \"baz\" r_string: \"qux\"");
+
+ private final int ignoreFieldNumber = getFieldNumber("o_int");
+
+ @Parameters(name = "{0}")
+ public static Collection<Object[]> parameters() {
+ return ProtoSubjectTestBase.parameters();
+ }
+
+ public IterableOfProtosSubjectTest(TestType testType) {
+ super(testType);
+ }
+
+ @Test
+ public void testPlain_isEmpty() {
+ expectThat(ImmutableList.<Message>of()).isEmpty();
+ expectThat(listOf(message1)).isNotEmpty();
+
+ expectFailureWhenTesting().that(listOf(message1)).isEmpty();
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting().that(ImmutableList.<Message>of()).isNotEmpty();
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testPlain_hasSize() {
+ expectThat(listOf(message1, message2)).hasSize(2);
+
+ expectFailureWhenTesting().that(listOf(message1)).hasSize(3);
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testPlain_containsNoDuplicates() {
+ expectThat(listOf(message1, message2)).containsNoDuplicates();
+
+ expectFailureWhenTesting().that(listOf(message1, eqMessage1)).containsNoDuplicates();
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testPlain_contains() {
+ expectThat(listOf(message1, message2)).contains(eqMessage2);
+ expectThat(listOf(message1, message2)).doesNotContain(eqIgnoredMessage1);
+
+ expectFailureWhenTesting().that(listOf(message1, message2)).contains(eqIgnoredMessage1);
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting().that(listOf(message1, message2)).doesNotContain(eqMessage1);
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testPlain_containsAny() {
+ expectThat(listOf(message1, message2)).containsAnyOf(eqIgnoredMessage1, eqMessage2);
+ expectThat(listOf(message1, message2)).containsAnyIn(listOf(eqIgnoredMessage1, eqMessage2));
+ expectThat(listOf(message1, message2)).containsAnyIn(arrayOf(eqIgnoredMessage1, eqMessage2));
+
+ expectFailureWhenTesting()
+ .that(listOf(message1, message2))
+ .containsAnyOf(eqIgnoredMessage1, eqIgnoredMessage2);
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(listOf(message1, message2))
+ .containsAnyIn(listOf(eqIgnoredMessage1, eqIgnoredMessage2));
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(listOf(message1, message2))
+ .containsAnyIn(arrayOf(eqIgnoredMessage1, eqIgnoredMessage2));
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testPlain_containsAtLeast() {
+ expectThat(listOf(message1, message2, eqIgnoredMessage1))
+ .containsAtLeast(eqMessage1, eqMessage2);
+ expectThat(listOf(message1, message2, eqIgnoredMessage1))
+ .containsAtLeastElementsIn(listOf(eqMessage1, eqMessage2));
+ expectThat(listOf(message1, message2, eqIgnoredMessage1))
+ .containsAtLeastElementsIn(arrayOf(eqMessage1, eqMessage2));
+
+ expectFailureWhenTesting().that(listOf(message1)).containsAtLeast(eqMessage1, eqMessage2);
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(listOf(message1))
+ .containsAtLeastElementsIn(listOf(eqMessage1, eqMessage2));
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(listOf(message1))
+ .containsAtLeastElementsIn(arrayOf(eqMessage1, eqMessage2));
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testPlain_containsExactly() {
+ expectThat(listOf(message1, message2)).containsExactly(eqMessage2, eqMessage1);
+ expectThat(listOf(message1, message2)).containsExactly(eqMessage1, eqMessage2).inOrder();
+ expectThat(listOf(message1, message2))
+ .containsExactlyElementsIn(listOf(eqMessage2, eqMessage1));
+ expectThat(listOf(message1, message2))
+ .containsExactlyElementsIn(listOf(eqMessage1, eqMessage2))
+ .inOrder();
+ expectThat(listOf(message1, message2))
+ .containsExactlyElementsIn(arrayOf(eqMessage2, eqMessage1));
+ expectThat(listOf(message1, message2))
+ .containsExactlyElementsIn(arrayOf(eqMessage1, eqMessage2))
+ .inOrder();
+
+ expectFailureWhenTesting().that(listOf(message1)).containsExactly(eqMessage1, eqMessage2);
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(listOf(message1, message2))
+ .containsExactly(eqMessage2, eqMessage1)
+ .inOrder();
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(listOf(message1))
+ .containsExactlyElementsIn(listOf(eqMessage1, eqMessage2));
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(listOf(message1, message2))
+ .containsExactlyElementsIn(listOf(eqMessage2, eqMessage1))
+ .inOrder();
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(listOf(message1))
+ .containsExactlyElementsIn(arrayOf(eqMessage1, eqMessage2));
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(listOf(message1, message2))
+ .containsExactlyElementsIn(arrayOf(eqMessage2, eqMessage1))
+ .inOrder();
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testPlain_containsNone() {
+ expectThat(listOf(message1)).containsNoneOf(eqMessage2, eqIgnoredMessage1);
+ expectThat(listOf(message1)).containsNoneIn(listOf(eqMessage2, eqIgnoredMessage1));
+ expectThat(listOf(message1)).containsNoneIn(arrayOf(eqMessage2, eqIgnoredMessage1));
+
+ expectFailureWhenTesting()
+ .that(listOf(message1, message2))
+ .containsNoneOf(eqMessage2, eqIgnoredMessage1);
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(listOf(message1, message2))
+ .containsNoneIn(listOf(eqMessage2, eqIgnoredMessage1));
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(listOf(message1, message2))
+ .containsNoneIn(arrayOf(eqMessage2, eqIgnoredMessage1));
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testPlain_isInOrder() {
+ expectThat(listOf(message1, eqMessage1, message2)).isInOrder(compareByOIntAscending());
+ expectThat(listOf(message1, message2)).isInStrictOrder(compareByOIntAscending());
+
+ expectFailureWhenTesting().that(listOf(message2, message1)).isInOrder(compareByOIntAscending());
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(listOf(message1, eqMessage1, message2))
+ .isInStrictOrder(compareByOIntAscending());
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testFluent_contains() {
+ expectThat(listOf(message1, message2))
+ .ignoringFields(ignoreFieldNumber)
+ .contains(eqIgnoredMessage1);
+ expectThat(listOf(message1, message2))
+ .ignoringRepeatedFieldOrder()
+ .doesNotContain(eqIgnoredMessage1);
+
+ expectFailureWhenTesting()
+ .that(listOf(message1, message2))
+ .ignoringFields(ignoreFieldNumber)
+ .contains(eqRepeatedMessage1);
+ expectThatFailure()
+ .factValue("testing whether")
+ .contains(
+ "is equivalent according to "
+ + "assertThat(proto)"
+ + ".ignoringFields("
+ + fullMessageName()
+ + ".o_int)"
+ + ".isEqualTo(target)");
+
+ expectFailureWhenTesting()
+ .that(listOf(message1, message2))
+ .ignoringRepeatedFieldOrder()
+ .doesNotContain(eqRepeatedMessage1);
+ expectThatFailure()
+ .factValue("testing whether")
+ .contains(
+ "is equivalent according to "
+ + "assertThat(proto).ignoringRepeatedFieldOrder().isEqualTo(target)");
+ }
+
+ @Test
+ public void testFluent_containsAny() {
+ expectThat(listOf(message1, message2))
+ .ignoringFields(ignoreFieldNumber)
+ .containsAnyOf(eqIgnoredMessage1, eqRepeatedMessage2);
+ expectThat(listOf(message1, message2))
+ .ignoringRepeatedFieldOrder()
+ .containsAnyIn(listOf(eqIgnoredMessage1, eqRepeatedMessage2));
+
+ expectFailureWhenTesting()
+ .that(listOf(message1, message2))
+ .ignoringFields(ignoreFieldNumber)
+ .containsAnyOf(eqRepeatedMessage1, eqRepeatedMessage2);
+ expectThatFailure()
+ .factValue("testing whether")
+ .contains(
+ "is equivalent according to "
+ + "assertThat(proto)"
+ + ".ignoringFields("
+ + fullMessageName()
+ + ".o_int)"
+ + ".isEqualTo(target)");
+
+ expectFailureWhenTesting()
+ .that(listOf(message1, message2))
+ .ignoringRepeatedFieldOrder()
+ .containsAnyIn(listOf(eqIgnoredMessage1, eqIgnoredMessage2));
+ expectThatFailure()
+ .factValue("testing whether")
+ .contains(
+ "is equivalent according to "
+ + "assertThat(proto).ignoringRepeatedFieldOrder().isEqualTo(target)");
+ }
+
+ @Test
+ public void testFluent_containsAtLeast() {
+ expectThat(listOf(message1, message2, eqRepeatedMessage2))
+ .ignoringFields(ignoreFieldNumber)
+ .containsAtLeast(eqIgnoredMessage1, eqIgnoredMessage2);
+ expectThat(listOf(message1, message2, eqIgnoredMessage1))
+ .ignoringRepeatedFieldOrder()
+ .containsAtLeastElementsIn(listOf(eqRepeatedMessage1, eqRepeatedMessage2));
+
+ expectFailureWhenTesting()
+ .that(listOf(message1))
+ .ignoringRepeatedFieldOrder()
+ .containsAtLeast(eqMessage1, eqMessage2);
+ expectThatFailure()
+ .factValue("testing whether")
+ .contains(
+ "is equivalent according to "
+ + "assertThat(proto).ignoringRepeatedFieldOrder().isEqualTo(target)");
+
+ expectFailureWhenTesting()
+ .that(listOf(message1))
+ .ignoringRepeatedFieldOrder()
+ .containsAtLeastElementsIn(listOf(eqMessage1, eqMessage2));
+ expectThatFailure()
+ .factValue("testing whether")
+ .contains(
+ "is equivalent according to "
+ + "assertThat(proto).ignoringRepeatedFieldOrder().isEqualTo(target)");
+ }
+
+ @Test
+ public void testFluent_containsExactly() {
+ expectThat(listOf(message1, message2))
+ .ignoringFields(ignoreFieldNumber)
+ .containsExactly(eqIgnoredMessage2, eqIgnoredMessage1);
+ expectThat(listOf(message1, message2))
+ .ignoringRepeatedFieldOrder()
+ .containsExactly(eqRepeatedMessage1, eqRepeatedMessage2)
+ .inOrder();
+ expectThat(listOf(message1, message2))
+ .ignoringFields(ignoreFieldNumber)
+ .containsExactlyElementsIn(listOf(eqIgnoredMessage2, eqIgnoredMessage1));
+ expectThat(listOf(message1, message2))
+ .ignoringRepeatedFieldOrder()
+ .containsExactlyElementsIn(listOf(eqRepeatedMessage1, eqRepeatedMessage2))
+ .inOrder();
+
+ expectFailureWhenTesting()
+ .that(listOf(message1))
+ .ignoringRepeatedFieldOrder()
+ .containsExactly(eqMessage1, eqMessage2);
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(listOf(message1, message2))
+ .ignoringRepeatedFieldOrder()
+ .containsExactly(eqMessage2, eqMessage1)
+ .inOrder();
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(listOf(message1))
+ .ignoringRepeatedFieldOrder()
+ .containsExactlyElementsIn(listOf(eqMessage1, eqMessage2));
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(listOf(message1, message2))
+ .ignoringRepeatedFieldOrder()
+ .containsExactlyElementsIn(listOf(eqMessage2, eqMessage1))
+ .inOrder();
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testFluent_containsNone() {
+ expectThat(listOf(message1))
+ .ignoringFields(ignoreFieldNumber)
+ .containsNoneOf(eqMessage2, eqRepeatedMessage1);
+ expectThat(listOf(message1))
+ .ignoringRepeatedFieldOrder()
+ .containsNoneIn(listOf(eqMessage2, eqIgnoredMessage1));
+
+ expectFailureWhenTesting()
+ .that(listOf(message1, message2))
+ .ignoringFields(ignoreFieldNumber)
+ .containsNoneOf(eqRepeatedMessage1, eqIgnoredMessage2);
+ expectThatFailure()
+ .factValue("testing whether")
+ .contains(
+ "is equivalent according to "
+ + "assertThat(proto)"
+ + ".ignoringFields("
+ + fullMessageName()
+ + ".o_int)"
+ + ".isEqualTo(target)");
+
+ expectFailureWhenTesting()
+ .that(listOf(message1, message2))
+ .ignoringRepeatedFieldOrder()
+ .containsNoneIn(listOf(eqIgnoredMessage1, eqRepeatedMessage2));
+ expectThatFailure()
+ .factValue("testing whether")
+ .contains(
+ "is equivalent according to "
+ + "assertThat(proto).ignoringRepeatedFieldOrder().isEqualTo(target)");
+ }
+
+ @Test
+ public void testFluent_correspondenceToString() {
+ // Some arbitrary tests to ensure Correspondence.toString() is well-behaved.
+ // Not intended to be comprehensive.
+
+ // TODO(user): Consider actually adding newlines as the strings are formatted here to make the
+ // error messages look prettier. Might require some thought to avoid eating too much vertical
+ // space, also indentation adds complexity.
+
+ expectFailureWhenTesting()
+ .that(listOf(message1))
+ .withPartialScope(FieldScopes.fromSetFields(message2))
+ .ignoringRepeatedFieldOrder()
+ .contains(message2);
+ expectThatFailure()
+ .factValue("testing whether")
+ .contains(
+ "assertThat(proto).withPartialScope(FieldScopes.fromSetFields({o_int: 3\n"
+ + "r_string: \"baz\"\n"
+ + "r_string: \"qux\"\n"
+ + "})).ignoringRepeatedFieldOrder().isEqualTo(target)");
+
+ expectFailureWhenTesting()
+ .that(listOf(message1))
+ .ignoringRepeatedFieldOrder()
+ .ignoringFieldScope(
+ FieldScopes.ignoringFields(getFieldNumber("o_int"), getFieldNumber("r_string")))
+ .ignoringFieldAbsence()
+ .contains(message2);
+ expectThatFailure()
+ .factValue("testing whether")
+ .contains(
+ "assertThat(proto)"
+ + ".ignoringRepeatedFieldOrder()"
+ + ".ignoringFieldScope("
+ + "FieldScopes.ignoringFields("
+ + fullMessageName()
+ + ".o_int, "
+ + fullMessageName()
+ + ".r_string))"
+ + ".ignoringFieldAbsence()"
+ + ".isEqualTo(target)");
+
+ expectFailureWhenTesting()
+ .that(listOf(message1))
+ .ignoringFields(getFieldNumber("o_enum"), getFieldNumber("o_test_message"))
+ .reportingMismatchesOnly()
+ .contains(message2);
+ expectThatFailure()
+ .factValue("testing whether")
+ .contains(
+ "assertThat(proto)"
+ + ".ignoringFields("
+ + fullMessageName()
+ + ".o_enum, "
+ + fullMessageName()
+ + ".o_test_message)"
+ + ".reportingMismatchesOnly()"
+ + ".isEqualTo(target)");
+ }
+
+ @Test
+ public void testFormatDiff() {
+ expectFailureWhenTesting()
+ .that(listOf(message1))
+ .ignoringRepeatedFieldOrder()
+ .containsExactly(message2);
+ expectThatFailure()
+ .factValue("diff")
+ .isEqualTo(
+ "Differences were found:\n"
+ + "modified: o_int: 3 -> 1\n"
+ + "added: r_string[0]: \"foo\"\n"
+ + "added: r_string[1]: \"bar\"\n"
+ + "deleted: r_string[0]: \"baz\"\n"
+ + "deleted: r_string[1]: \"qux\"\n");
+ }
+
+ @Test
+ public void testDisplayingDiffsPairedBy() {
+ Message actualInt3 = parse("o_int: 3 r_string: 'foo'");
+ Message actualInt4 = parse("o_int: 4 r_string: 'bar'");
+ Message expectedInt3 = parse("o_int: 3 r_string: 'baz'");
+ Message expectedInt4 = parse("o_int: 4 r_string: 'qux'");
+
+ Function<Message, Integer> getInt =
+ new Function<Message, Integer>() {
+ @Override
+ public Integer apply(Message message) {
+ return (Integer) message.getField(getFieldDescriptor("o_int"));
+ }
+ };
+
+ expectFailureWhenTesting()
+ .that(listOf(actualInt3, actualInt4))
+ .displayingDiffsPairedBy(getInt)
+ .containsExactly(expectedInt3, expectedInt4);
+ expectThatFailure().factValue("diff", 0).contains("modified: r_string[0]: \"baz\" -> \"foo\"");
+ expectThatFailure().factValue("diff", 1).contains("modified: r_string[0]: \"qux\" -> \"bar\"");
+ }
+
+ @Test
+ public void testCompareMultipleMessageTypes() {
+ // Don't run this test twice.
+ if (!testIsRunOnce()) {
+ return;
+ }
+
+ expectThat(
+ listOf(
+ TestMessage2.newBuilder().addRString("foo").addRString("bar").build(),
+ TestMessage3.newBuilder().addRString("baz").addRString("qux").build()))
+ .ignoringRepeatedFieldOrder()
+ .containsExactly(
+ TestMessage3.newBuilder().addRString("qux").addRString("baz").build(),
+ TestMessage2.newBuilder().addRString("bar").addRString("foo").build());
+ }
+
+ private Comparator<Message> compareByOIntAscending() {
+ return new Comparator<Message>() {
+ @Override
+ public int compare(Message message1, Message message2) {
+ return Integer.compare(
+ (Integer) message1.getField(getFieldDescriptor("o_int")),
+ (Integer) message2.getField(getFieldDescriptor("o_int")));
+ }
+ };
+ }
+}
diff --git a/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/MapWithProtoValuesSubjectTest.java b/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/MapWithProtoValuesSubjectTest.java
new file mode 100644
index 00000000..df855487
--- /dev/null
+++ b/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/MapWithProtoValuesSubjectTest.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.truth.MapSubject;
+import com.google.protobuf.Message;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Tests for {@link MapWithProtoValuesSubject}.
+ *
+ * <p>Individual equality fuzzing is thoroughly tested by {@link ProtoSubjectTest}, while fuzzy
+ * equality testing is thoroughly tested by {@link com.google.common.truth.MapSubjectTest}. Thus, we
+ * simply check that all of the exposed methods work in basic cases, and trust that the
+ * implementation ensures correctness in the cross-product of the many ways one can do things.
+ */
+@RunWith(Parameterized.class)
+public class MapWithProtoValuesSubjectTest extends ProtoSubjectTestBase {
+
+ private final Message message1 = parse("o_int: 1 r_string: \"foo\" r_string: \"bar\"");
+ private final Message eqMessage1 = parse("o_int: 1 r_string: \"foo\" r_string: \"bar\"");
+ private final Message eqRepeatedMessage1 = parse("o_int: 1 r_string: \"bar\" r_string: \"foo\"");
+ private final Message eqIgnoredMessage1 = parse("o_int: 2 r_string: \"foo\" r_string: \"bar\"");
+ private final Message message2 = parse("o_int: 3 r_string: \"baz\" r_string: \"qux\"");
+ private final Message eqMessage2 = parse("o_int: 3 r_string: \"baz\" r_string: \"qux\"");
+ private final Message eqRepeatedMessage2 = parse("o_int: 3 r_string: \"qux\" r_string: \"baz\"");
+ private final Message eqIgnoredMessage2 = parse("o_int: 4 r_string: \"baz\" r_string: \"qux\"");
+
+ private final int ignoreFieldNumber = getFieldNumber("o_int");
+
+ @Parameters(name = "{0}")
+ public static Collection<Object[]> parameters() {
+ return ProtoSubjectTestBase.parameters();
+ }
+
+ public MapWithProtoValuesSubjectTest(TestType testType) {
+ super(testType);
+ }
+
+ @Test
+ public void testPlain_isEqualTo() {
+ expectThat(mapOf(1, message1, 2, message2)).isEqualTo(mapOf(2, eqMessage2, 1, eqMessage1));
+ expectThat(mapOf(1, message2)).isNotEqualTo(mapOf(1, message1));
+
+ expectFailureWhenTesting()
+ .that(mapOf(1, message2, 2, message1))
+ .isEqualTo(mapOf(1, eqMessage1, 2, eqMessage2));
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting().that(mapOf(1, message1)).isNotEqualTo(mapOf(1, eqMessage1));
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testPlain_isEmpty() {
+ expectThat(ImmutableMap.<Object, Message>of()).isEmpty();
+ expectThat(mapOf(1, message1)).isNotEmpty();
+
+ expectFailureWhenTesting().that(mapOf(1, message1)).isEmpty();
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting().that(ImmutableMap.<Object, Message>of()).isNotEmpty();
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testPlain_hasSize() {
+ expectThat(mapOf(1, message1, 2, message2)).hasSize(2);
+
+ expectFailureWhenTesting().that(mapOf(1, message1)).hasSize(3);
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testPlain_containsKey() {
+ expectThat(mapOf(1, message1, 2, message2)).containsKey(1);
+ expectThat(mapOf(1, message1, 2, message2)).doesNotContainKey(3);
+
+ expectFailureWhenTesting().that(mapOf(1, message1, 2, message2)).containsKey(3);
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting().that(mapOf(1, message1, 2, message2)).doesNotContainKey(2);
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testPlain_containsEntry() {
+ expectThat(mapOf(1, message1, 2, message2)).containsEntry(2, eqMessage2);
+ expectThat(mapOf(1, message1, 2, message2)).doesNotContainEntry(1, eqMessage2);
+
+ expectFailureWhenTesting().that(mapOf(1, message1, 2, message2)).containsEntry(2, eqMessage1);
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(mapOf(1, message1, 2, message2))
+ .doesNotContainEntry(2, eqMessage2);
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testPlain_containsExactly() {
+ expectThat(mapOf(1, message1, 2, message2)).containsExactly(2, eqMessage2, 1, eqMessage1);
+ expectThat(mapOf(1, message1, 2, message2))
+ .containsExactly(1, eqMessage1, 2, eqMessage2)
+ .inOrder();
+ expectThat(mapOf(1, message1, 2, message2))
+ .containsExactlyEntriesIn(mapOf(2, eqMessage2, 1, eqMessage1));
+ expectThat(mapOf(1, message1, 2, message2))
+ .containsExactlyEntriesIn(mapOf(1, eqMessage1, 2, eqMessage2))
+ .inOrder();
+
+ expectFailureWhenTesting()
+ .that(mapOf(1, message1))
+ .containsExactly(1, eqMessage1, 2, eqMessage2);
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(mapOf(1, message1, 2, message2))
+ .containsExactly(2, eqMessage2, 1, eqMessage1)
+ .inOrder();
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(mapOf(1, message1))
+ .containsExactlyEntriesIn(mapOf(2, eqMessage2, 1, eqMessage1));
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(mapOf(1, message1, 2, message2))
+ .containsExactlyEntriesIn(mapOf(2, eqMessage2, 1, eqMessage1))
+ .inOrder();
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testFluent_containsEntry() {
+ expectThat(mapOf(1, message1, 2, message2))
+ .ignoringFieldsForValues(ignoreFieldNumber)
+ .containsEntry(1, eqIgnoredMessage1);
+ expectThat(mapOf(1, message1, 2, message2))
+ .ignoringRepeatedFieldOrderForValues()
+ .doesNotContainEntry(1, eqIgnoredMessage1);
+
+ expectFailureWhenTesting()
+ .that(mapOf(1, message1, 2, message2))
+ .ignoringFieldsForValues(ignoreFieldNumber)
+ .containsEntry(1, eqRepeatedMessage1);
+ expectThatFailure()
+ .hasMessageThat()
+ .contains(
+ "is equivalent according to "
+ + "assertThat(proto)"
+ + ".ignoringFields("
+ + fullMessageName()
+ + ".o_int)"
+ + ".isEqualTo(target)");
+
+ expectFailureWhenTesting()
+ .that(mapOf(1, message1, 2, message2))
+ .ignoringRepeatedFieldOrderForValues()
+ .doesNotContainEntry(1, eqRepeatedMessage1);
+ expectThatFailure()
+ .hasMessageThat()
+ .contains(
+ "is equivalent according to "
+ + "assertThat(proto).ignoringRepeatedFieldOrder().isEqualTo(target)");
+ }
+
+ @Test
+ public void testFluent_containsExactly() {
+ expectThat(mapOf(1, message1, 2, message2))
+ .ignoringFieldsForValues(ignoreFieldNumber)
+ .containsExactly(2, eqIgnoredMessage2, 1, eqIgnoredMessage1);
+ expectThat(mapOf(1, message1, 2, message2))
+ .ignoringRepeatedFieldOrderForValues()
+ .containsExactly(1, eqRepeatedMessage1, 2, eqRepeatedMessage2)
+ .inOrder();
+ expectThat(mapOf(1, message1, 2, message2))
+ .ignoringFieldsForValues(ignoreFieldNumber)
+ .containsExactlyEntriesIn(mapOf(2, eqIgnoredMessage2, 1, eqIgnoredMessage1));
+ expectThat(mapOf(1, message1, 2, message2))
+ .ignoringRepeatedFieldOrderForValues()
+ .containsExactlyEntriesIn(mapOf(1, eqRepeatedMessage1, 2, eqRepeatedMessage2))
+ .inOrder();
+
+ expectFailureWhenTesting()
+ .that(mapOf(1, message1))
+ .ignoringRepeatedFieldOrderForValues()
+ .containsExactly(1, eqRepeatedMessage1, 2, eqMessage2);
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(mapOf(1, message1, 2, message2))
+ .ignoringFieldsForValues(ignoreFieldNumber)
+ .containsExactly(2, eqIgnoredMessage2, 1, eqIgnoredMessage1)
+ .inOrder();
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(mapOf(1, message1))
+ .ignoringRepeatedFieldOrderForValues()
+ .containsExactlyEntriesIn(mapOf(2, eqRepeatedMessage2, 1, eqRepeatedMessage1));
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(mapOf(1, message1, 2, message2))
+ .ignoringFieldsForValues(ignoreFieldNumber)
+ .containsExactlyEntriesIn(mapOf(2, eqIgnoredMessage2, 1, eqIgnoredMessage1))
+ .inOrder();
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testCompareMultipleMessageTypes() {
+ // Don't run this test twice.
+ if (!testIsRunOnce()) {
+ return;
+ }
+
+ expectThat(
+ ImmutableMap.of(
+ 2,
+ TestMessage2.newBuilder().addRString("foo").addRString("bar").build(),
+ 3,
+ TestMessage3.newBuilder().addRString("baz").addRString("qux").build()))
+ .ignoringRepeatedFieldOrderForValues()
+ .containsExactly(
+ 3, TestMessage3.newBuilder().addRString("qux").addRString("baz").build(),
+ 2, TestMessage2.newBuilder().addRString("bar").addRString("foo").build());
+ }
+
+ @Test
+ public void testMethodNamesEndWithForValues() {
+ checkMethodNamesEndWithForValues(MapWithProtoValuesSubject.class, MapSubject.class);
+ checkMethodNamesEndWithForValues(MapWithProtoValuesFluentAssertion.class, MapSubject.class);
+ }
+}
diff --git a/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/MultiExpectFailure.java b/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/MultiExpectFailure.java
new file mode 100644
index 00000000..d1ae1dd5
--- /dev/null
+++ b/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/MultiExpectFailure.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2018 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import com.google.common.base.Preconditions;
+import com.google.common.truth.ExpectFailure;
+import com.google.common.truth.StandardSubjectBuilder;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * A collection of {@link ExpectFailure} rules, for use in a single test case.
+ *
+ * <p>Users should instantiate {@code MultiExpectFailure} as a {@code @Rule}, then use {@link
+ * #whenTesting()} and {@link #getFailure()} just like you would with an ordinary {@link
+ * ExpectFailure} rule. Each call to {@link #whenTesting()} will clobber the previous {@link
+ * #getFailure()} results.
+ */
+class MultiExpectFailure implements TestRule {
+
+ private final List<ExpectFailure> expectFailures;
+ private int currentIndex = -1;
+
+ MultiExpectFailure(int size) {
+ expectFailures = new ArrayList<>(size);
+ for (int i = 0; i < size; i++) {
+ expectFailures.add(new ExpectFailure());
+ }
+ }
+
+ StandardSubjectBuilder whenTesting() {
+ Preconditions.checkState(
+ currentIndex < expectFailures.size() - 1,
+ "Not enough ExpectFailures (%s)",
+ expectFailures.size());
+ return expectFailures.get(++currentIndex).whenTesting();
+ }
+
+ AssertionError getFailure() {
+ Preconditions.checkState(currentIndex >= 0, "Must call 'whenTesting()' first.");
+ return expectFailures.get(currentIndex).getFailure();
+ }
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+ Statement statement = base;
+ for (ExpectFailure expectFailure : expectFailures) {
+ statement = expectFailure.apply(statement, description);
+ }
+ return statement;
+ }
+}
diff --git a/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/MultimapWithProtoValuesSubjectTest.java b/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/MultimapWithProtoValuesSubjectTest.java
new file mode 100644
index 00000000..d2bd5cd8
--- /dev/null
+++ b/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/MultimapWithProtoValuesSubjectTest.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.truth.MultimapSubject;
+import com.google.protobuf.Message;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Tests for {@link MultimapWithProtoValuesSubject}.
+ *
+ * <p>Individual equality fuzzing is thoroughly tested by {@link ProtoSubjectTest}, while fuzzy
+ * equality testing is thoroughly tested by {@link com.google.common.truth.MultimapSubjectTest}.
+ * Thus, we simply check that all of the exposed methods work in basic cases, and trust that the
+ * implementation ensures correctness in the cross-product of the many ways one can do things.
+ */
+@RunWith(Parameterized.class)
+public class MultimapWithProtoValuesSubjectTest extends ProtoSubjectTestBase {
+
+ private final Message message1 = parse("o_int: 1 r_string: \"foo\" r_string: \"bar\"");
+ private final Message eqMessage1 = parse("o_int: 1 r_string: \"foo\" r_string: \"bar\"");
+ private final Message eqRepeatedMessage1 = parse("o_int: 1 r_string: \"bar\" r_string: \"foo\"");
+ private final Message eqIgnoredMessage1 = parse("o_int: 2 r_string: \"foo\" r_string: \"bar\"");
+ private final Message message2 = parse("o_int: 3 r_string: \"baz\" r_string: \"qux\"");
+ private final Message eqMessage2 = parse("o_int: 3 r_string: \"baz\" r_string: \"qux\"");
+ private final Message eqRepeatedMessage2 = parse("o_int: 3 r_string: \"qux\" r_string: \"baz\"");
+ private final Message eqIgnoredMessage2 = parse("o_int: 4 r_string: \"baz\" r_string: \"qux\"");
+
+ private final int ignoreFieldNumber = getFieldNumber("o_int");
+
+ @Parameters(name = "{0}")
+ public static Collection<Object[]> parameters() {
+ return ProtoSubjectTestBase.parameters();
+ }
+
+ public MultimapWithProtoValuesSubjectTest(TestType testType) {
+ super(testType);
+ }
+
+ @Test
+ public void testPlain_isEmpty() {
+ expectThat(ImmutableMultimap.<Object, Message>of()).isEmpty();
+ expectThat(multimapOf(1, message1)).isNotEmpty();
+
+ expectFailureWhenTesting().that(multimapOf(1, message1)).isEmpty();
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting().that(ImmutableMap.<Object, Message>of()).isNotEmpty();
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testPlain_hasSize() {
+ expectThat(multimapOf(1, message1, 1, message2, 2, message1)).hasSize(3);
+
+ expectFailureWhenTesting().that(multimapOf(1, message1)).hasSize(3);
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testPlain_containsKey() {
+ expectThat(multimapOf(1, message1, 1, message2, 2, message1)).containsKey(1);
+ expectThat(multimapOf(1, message1, 1, message2, 2, message1)).doesNotContainKey(3);
+
+ expectFailureWhenTesting()
+ .that(multimapOf(1, message1, 1, message2, 2, message1))
+ .containsKey(3);
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(multimapOf(1, message1, 1, message2, 2, message1))
+ .doesNotContainKey(2);
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testPlain_containsEntry() {
+ expectThat(multimapOf(1, message1, 1, message2, 2, message1)).containsEntry(1, eqMessage2);
+ expectThat(multimapOf(1, message1, 1, message2, 2, message1))
+ .doesNotContainEntry(2, eqMessage2);
+
+ expectFailureWhenTesting()
+ .that(multimapOf(1, message1, 1, message2, 2, message1))
+ .containsEntry(2, eqMessage2);
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(multimapOf(1, message1, 1, message2, 2, message1))
+ .doesNotContainEntry(1, eqMessage2);
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testPlain_containsExactlyEntriesIn() {
+ expectThat(multimapOf(1, message1, 1, message2, 2, message1))
+ .containsExactlyEntriesIn(multimapOf(1, eqMessage2, 2, eqMessage1, 1, eqMessage1));
+ expectThat(multimapOf(1, message1, 1, message2, 2, message1))
+ .containsExactlyEntriesIn(multimapOf(1, eqMessage1, 1, eqMessage2, 2, eqMessage1))
+ .inOrder();
+
+ expectFailureWhenTesting()
+ .that(multimapOf(1, message1))
+ .containsExactlyEntriesIn(multimapOf(1, eqMessage1, 2, eqMessage2));
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(multimapOf(1, message1, 2, message2))
+ .containsExactlyEntriesIn(multimapOf(2, eqMessage2, 1, eqMessage1))
+ .inOrder();
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testPlain_containsExactlyNoArgs() {
+ expectThat(ImmutableMultimap.<Object, Message>of()).containsExactly();
+ expectThat(ImmutableMultimap.<Object, Message>of()).containsExactly().inOrder();
+
+ expectFailureWhenTesting().that(multimapOf(1, message1)).containsExactly();
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testPlain_containsExactly() {
+ expectThat(multimapOf(1, message1, 1, message2, 2, message1))
+ .containsExactly(1, eqMessage2, 2, eqMessage1, 1, eqMessage1);
+ expectThat(multimapOf(1, message1, 1, message2, 2, message1))
+ .containsExactly(1, eqMessage1, 1, eqMessage2, 2, eqMessage1)
+ .inOrder();
+
+ expectFailureWhenTesting()
+ .that(multimapOf(1, message1))
+ .containsExactly(1, eqMessage1, 2, eqMessage2);
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(multimapOf(1, message1, 2, message2))
+ .containsExactly(2, eqMessage2, 1, eqMessage1)
+ .inOrder();
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testPlain_valuesForKey() {
+ expectThat(multimapOf(1, message1, 1, message2, 2, message1))
+ .valuesForKey(1)
+ .containsExactly(eqMessage2, eqMessage1);
+ expectThat(multimapOf(1, message1, 1, message2, 2, message1)).valuesForKey(2).hasSize(1);
+
+ expectFailureWhenTesting()
+ .that(multimapOf(1, message1, 1, message2, 2, message1))
+ .valuesForKey(1)
+ .containsExactly(eqMessage2, eqMessage1)
+ .inOrder();
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testFluent_containsEntry() {
+ expectThat(multimapOf(1, message1, 1, message2, 2, message1))
+ .ignoringFieldsForValues(ignoreFieldNumber)
+ .containsEntry(1, eqIgnoredMessage2);
+ expectThat(multimapOf(1, message1, 1, message2, 2, message1))
+ .ignoringRepeatedFieldOrderForValues()
+ .doesNotContainEntry(1, eqIgnoredMessage2);
+
+ expectFailureWhenTesting()
+ .that(multimapOf(1, message1, 1, message2, 2, message1))
+ .ignoringFieldsForValues(ignoreFieldNumber)
+ .containsEntry(1, eqRepeatedMessage2);
+ expectThatFailure()
+ .hasMessageThat()
+ .contains(
+ "is equivalent according to "
+ + "assertThat(proto)"
+ + ".ignoringFields("
+ + fullMessageName()
+ + ".o_int)"
+ + ".isEqualTo(target)");
+
+ expectFailureWhenTesting()
+ .that(multimapOf(1, message1, 1, message2, 2, message1))
+ .ignoringRepeatedFieldOrderForValues()
+ .doesNotContainEntry(1, eqRepeatedMessage2);
+ expectThatFailure()
+ .hasMessageThat()
+ .contains(
+ "is equivalent according to "
+ + "assertThat(proto).ignoringRepeatedFieldOrder().isEqualTo(target)");
+ }
+
+ @Test
+ public void testFluent_containsExactlyEntriesIn() {
+ expectThat(multimapOf(1, message1, 1, message2, 2, message1))
+ .ignoringFieldsForValues(ignoreFieldNumber)
+ .containsExactlyEntriesIn(
+ multimapOf(1, eqIgnoredMessage2, 2, eqIgnoredMessage1, 1, eqIgnoredMessage1));
+ expectThat(multimapOf(1, message1, 1, message2, 2, message1))
+ .ignoringRepeatedFieldOrderForValues()
+ .containsExactlyEntriesIn(
+ multimapOf(1, eqRepeatedMessage1, 1, eqRepeatedMessage2, 2, eqRepeatedMessage1))
+ .inOrder();
+
+ expectFailureWhenTesting()
+ .that(multimapOf(1, message1))
+ .ignoringRepeatedFieldOrderForValues()
+ .containsExactlyEntriesIn(multimapOf(2, eqRepeatedMessage2, 1, eqRepeatedMessage1));
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(multimapOf(1, message1, 2, message2))
+ .ignoringFieldsForValues(ignoreFieldNumber)
+ .containsExactlyEntriesIn(multimapOf(2, eqIgnoredMessage2, 1, eqIgnoredMessage1))
+ .inOrder();
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testFluent_containsExactly_noArgs() {
+ expectThat(ImmutableMultimap.<Object, Message>of())
+ .ignoringRepeatedFieldOrderForValues()
+ .containsExactly();
+ expectThat(ImmutableMultimap.<Object, Message>of())
+ .ignoringRepeatedFieldOrderForValues()
+ .containsExactly()
+ .inOrder();
+
+ expectFailureWhenTesting()
+ .that(multimapOf(1, message1))
+ .ignoringRepeatedFieldOrderForValues()
+ .containsExactly();
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testFluent_containsExactly() {
+ expectThat(multimapOf(1, message1, 1, message2, 2, message1))
+ .ignoringFieldsForValues(ignoreFieldNumber)
+ .containsExactly(1, eqIgnoredMessage2, 2, eqIgnoredMessage1, 1, eqIgnoredMessage1);
+ expectThat(multimapOf(1, message1, 1, message2, 2, message1))
+ .ignoringRepeatedFieldOrderForValues()
+ .containsExactly(1, eqRepeatedMessage1, 1, eqRepeatedMessage2, 2, eqRepeatedMessage1)
+ .inOrder();
+
+ expectFailureWhenTesting()
+ .that(multimapOf(1, message1))
+ .ignoringRepeatedFieldOrderForValues()
+ .containsExactly(2, eqRepeatedMessage2, 1, eqRepeatedMessage1);
+ expectThatFailure().isNotNull();
+
+ expectFailureWhenTesting()
+ .that(multimapOf(1, message1, 2, message2))
+ .ignoringFieldsForValues(ignoreFieldNumber)
+ .containsExactly(2, eqIgnoredMessage2, 1, eqIgnoredMessage1)
+ .inOrder();
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testFluent_valuesForKey() {
+ expectThat(multimapOf(1, message1, 1, message2, 2, message1))
+ .valuesForKey(1)
+ .ignoringFields(ignoreFieldNumber)
+ .containsExactly(eqIgnoredMessage2, eqIgnoredMessage1);
+ expectThat(multimapOf(1, message1, 1, message2, 2, message1))
+ .valuesForKey(2)
+ .ignoringRepeatedFieldOrder()
+ .containsExactly(eqRepeatedMessage1);
+
+ expectFailureWhenTesting()
+ .that(multimapOf(1, message1, 1, message2, 2, message1))
+ .valuesForKey(1)
+ .ignoringFields(ignoreFieldNumber)
+ .containsExactly(eqRepeatedMessage1, eqRepeatedMessage2);
+ expectThatFailure().isNotNull();
+ }
+
+ @Test
+ public void testCompareMultipleMessageTypes() {
+ // Don't run this test twice.
+ if (!testIsRunOnce()) {
+ return;
+ }
+
+ expectThat(
+ ImmutableMultimap.of(
+ 2,
+ TestMessage2.newBuilder().addRString("foo").addRString("bar").build(),
+ 2,
+ TestMessage2.newBuilder().addRString("quibble").addRString("frozzit").build(),
+ 3,
+ TestMessage3.newBuilder().addRString("baz").addRString("qux").build()))
+ .ignoringRepeatedFieldOrderForValues()
+ .containsExactlyEntriesIn(
+ ImmutableMultimap.of(
+ 2,
+ TestMessage2.newBuilder().addRString("frozzit").addRString("quibble").build(),
+ 3,
+ TestMessage3.newBuilder().addRString("qux").addRString("baz").build(),
+ 2,
+ TestMessage2.newBuilder().addRString("bar").addRString("foo").build()));
+ }
+
+ @Test
+ public void testMethodNamesEndWithForValues() {
+ checkMethodNamesEndWithForValues(MultimapWithProtoValuesSubject.class, MultimapSubject.class);
+ checkMethodNamesEndWithForValues(
+ MultimapWithProtoValuesFluentAssertion.class, MultimapSubject.class);
+ }
+}
diff --git a/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/OverloadResolutionTest.java b/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/OverloadResolutionTest.java
new file mode 100644
index 00000000..c5dea67c
--- /dev/null
+++ b/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/OverloadResolutionTest.java
@@ -0,0 +1,363 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import static com.google.common.truth.Truth.assertAbout;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.protos;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Test to ensure that Truth.assertThat and ProtoTruth.assertThat can coexist while statically
+ * imported. The tests themselves are simple and dumb, as what's really being tested here is whether
+ * or not this file compiles.
+ */
+@RunWith(JUnit4.class)
+public class OverloadResolutionTest extends ProtoSubjectTestBase {
+ public OverloadResolutionTest() {
+ // We don't bother testing Proto3 because it's immaterial to this test, and we want to ensure
+ // that using Iterable<MyMessage> works, not just Iterable<Message>.
+ super(TestType.IMMUTABLE_PROTO2);
+ }
+
+ @Override
+ protected TestMessage2 parse(String string) {
+ return (TestMessage2) super.parse(string);
+ }
+
+ @Test
+ public void testObjectOverloads_testMessages_normalMethods() {
+ TestMessage2 message = parse("r_string: \"foo\" r_string: \"bar\"");
+ TestMessage2 eqMessage = parse("r_string: \"foo\" r_string: \"bar\"");
+ TestMessage2 diffMessage = parse("r_string: \"bar\" r_string: \"foo\"");
+ Object object = message;
+ Object eqObject = eqMessage;
+ Object diffObject = diffMessage;
+
+ assertThat(message).isSameInstanceAs(object);
+ assertThat(message).isNotSameInstanceAs(eqMessage);
+ assertThat(message).isEqualTo(eqMessage);
+ assertThat(message).isNotEqualTo(diffMessage);
+ assertThat(message).isEqualTo(eqObject);
+ assertThat(message).isNotEqualTo(diffObject);
+ }
+
+ @Test
+ public void testObjectOverloads_testMessages_specializedMethods() {
+ TestMessage2 message = parse("r_string: \"foo\" r_string: \"bar\"");
+ TestMessage2 diffMessage = parse("r_string: \"bar\" r_string: \"foo\"");
+
+ assertThat(message).ignoringRepeatedFieldOrder().isEqualTo(diffMessage);
+ }
+
+ @Test
+ public void testObjectOverloads_objects_actuallyMessages() {
+ TestMessage2 message = parse("r_string: \"foo\" r_string: \"bar\"");
+ TestMessage2 eqMessage = parse("r_string: \"foo\" r_string: \"bar\"");
+ TestMessage2 diffMessage = parse("r_string: \"bar\" r_string: \"foo\"");
+ Object object = message;
+ Object eqObject = eqMessage;
+ Object diffObject = diffMessage;
+
+ assertThat(object).isSameInstanceAs(message);
+ assertThat(object).isNotSameInstanceAs(eqObject);
+ assertThat(object).isEqualTo(eqObject);
+ assertThat(object).isNotEqualTo(diffObject);
+ assertThat(object).isEqualTo(eqMessage);
+ assertThat(object).isNotEqualTo(diffMessage);
+ }
+
+ @Test
+ public void testObjectOverloads_objects_actuallyNotMessages() {
+ TestMessage2 message = parse("r_string: \"foo\" r_string: \"bar\"");
+ Object altObject = 1111;
+ Object eqAltObject = (1 + 10 + 100 + 1000);
+
+ assertThat(altObject).isEqualTo(eqAltObject);
+ assertThat(altObject).isNotEqualTo(message);
+ }
+
+ @Test
+ public void testIterableOverloads_assertAbout() {
+ TestMessage2 message1 = parse("o_int: 1 r_string: \"foo\"");
+ TestMessage2 message2 = parse("o_int: 2 r_string: \"bar\"");
+ TestMessage2 eqMessage2 = parse("o_int: 2 r_string: \"bar\"");
+
+ assertAbout(protos()).that(listOf(message1, message2)).contains(eqMessage2);
+ }
+
+ @Test
+ public void testIterableOverloads_testMessages_normalMethods() {
+ TestMessage2 message1 = parse("o_int: 1 r_string: \"foo\"");
+ TestMessage2 message2 = parse("o_int: 2 r_string: \"bar\"");
+ TestMessage2 eqMessage2 = parse("o_int: 2 r_string: \"bar\"");
+ TestMessage2 message3 = parse("o_int: 3 r_string: \"baz\"");
+ TestMessage2 message4 = parse("o_int: 4 r_string: \"qux\"");
+ Object object1 = message1;
+ Object object2 = message2;
+ ImmutableSet<TestMessage2> actualMessages = ImmutableSet.of(message1, message2);
+
+ assertThat(actualMessages).containsExactly(message1, message2).inOrder();
+ assertThat(actualMessages).containsExactly(object1, object2).inOrder();
+ assertThat(actualMessages).hasSize(2);
+ assertThat(actualMessages).containsAnyOf(message3, eqMessage2, message4);
+ }
+
+ @Test
+ public void testIterableOverloads_testMessages_specializedMethods() {
+ TestMessage2 message1 = parse("o_int: 1 r_string: \"foo\"");
+ TestMessage2 message2 = parse("o_int: 2 r_string: \"bar\"");
+ TestMessage2 message3 = parse("o_int: 3 r_string: \"baz\"");
+ TestMessage2 message4 = parse("o_int: 4 r_string: \"qux\"");
+ ImmutableSet<TestMessage2> actualMessages = ImmutableSet.of(message1, message2);
+
+ assertThat(actualMessages)
+ .ignoringFields(getFieldNumber("o_int"), getFieldNumber("r_string"))
+ .containsExactly(message3, message4)
+ .inOrder();
+ }
+
+ @Test
+ public void testIterableOverloads_objects_actuallyMessages() {
+ TestMessage2 message1 = parse("o_int: 1 r_string: \"foo\"");
+ TestMessage2 message2 = parse("o_int: 2 r_string: \"bar\"");
+ TestMessage2 eqMessage2 = parse("o_int: 2 r_string: \"bar\"");
+ TestMessage2 message3 = parse("o_int: 3 r_string: \"baz\"");
+ TestMessage2 message4 = parse("o_int: 4 r_string: \"qux\"");
+ Object object1 = message1;
+ Object object2 = message2;
+ ImmutableList<Object> actualObjects = ImmutableList.of(object1, object2);
+
+ assertThat(actualObjects).containsExactly(message1, message2).inOrder();
+ assertThat(actualObjects).hasSize(2);
+ assertThat(actualObjects).containsAnyOf(message3, eqMessage2, message4);
+ }
+
+ @Test
+ public void testIterableOverloads_objects_actuallyNotMessages() {
+ TestMessage2 message1 = TestMessage2.newBuilder().setOInt(1).addRString("foo").build();
+ TestMessage2 message2 = TestMessage2.newBuilder().setOInt(2).addRString("bar").build();
+ ImmutableList<Object> altActualObjects = ImmutableList.<Object>of("Foo!", 42);
+
+ assertThat(altActualObjects).containsExactly(21 * 2, "Foo! Bar!".substring(0, 4));
+ assertThat(altActualObjects).containsNoneOf(message1, message2);
+ }
+
+ @Test
+ public void testMapOverloads_assertAbout() {
+ TestMessage2 message1 = parse("o_int: 1 r_string: \"foo\"");
+ TestMessage2 message2 = parse("o_int: 2 r_string: \"bar\"");
+ TestMessage2 eqMessage2 = parse("o_int: 2 r_string: \"bar\"");
+
+ assertAbout(protos()).that(mapOf(1, message1, 2, message2)).containsEntry(2, eqMessage2);
+ }
+
+ @Test
+ public void testMapOverloads_testMessages_normalMethods() {
+ TestMessage2 message1 = parse("o_int: 1 r_string: \"foo\"");
+ TestMessage2 message2 = parse("o_int: 2 r_string: \"bar\"");
+ TestMessage2 eqMessage2 = parse("o_int: 2 r_string: \"bar\"");
+ Object object1 = message1;
+ Object object2 = message2;
+ ImmutableMap<Integer, TestMessage2> actualMessages = mapOf(1, message1, 2, message2);
+
+ assertThat(actualMessages).containsExactly(1, message1, 2, message2).inOrder();
+ assertThat(actualMessages).containsExactly(1, object1, 2, object2).inOrder();
+ assertThat(actualMessages).hasSize(2);
+ assertThat(actualMessages).isEqualTo(mapOf(2, eqMessage2, 1, object1));
+ assertThat(actualMessages).isNotEqualTo(mapOf(1, object2, 2, object1));
+ }
+
+ @Test
+ public void testMapOverloads_testMessages_specializedMethods() {
+ TestMessage2 message1 = parse("o_int: 1 r_string: \"foo\"");
+ TestMessage2 message2 = parse("o_int: 2 r_string: \"bar\"");
+ TestMessage2 message3 = parse("o_int: 3 r_string: \"baz\"");
+ TestMessage2 message4 = parse("o_int: 4 r_string: \"qux\"");
+ ImmutableMap<Integer, TestMessage2> actualMessages = mapOf(1, message1, 2, message2);
+
+ assertThat(actualMessages)
+ .ignoringFieldsForValues(getFieldNumber("o_int"), getFieldNumber("r_string"))
+ .containsExactly(1, message3, 2, message4)
+ .inOrder();
+ }
+
+ @Test
+ public void testMapOverloads_objects_actuallyMessages() {
+ TestMessage2 message1 = parse("o_int: 1 r_string: \"foo\"");
+ TestMessage2 message2 = parse("o_int: 2 r_string: \"bar\"");
+ TestMessage2 eqMessage2 = parse("o_int: 2 r_string: \"bar\"");
+ Object object1 = message1;
+ Object object2 = message2;
+ ImmutableMap<String, Object> actualObjects = mapOf("a", object1, "b", object2);
+
+ assertThat(actualObjects).containsExactly("a", message1, "b", message2).inOrder();
+ assertThat(actualObjects).hasSize(2);
+ assertThat(actualObjects).containsEntry("b", eqMessage2);
+ }
+
+ @Test
+ public void testMapOverloads_objects_actuallyNotMessages() {
+ TestMessage2 message1 = TestMessage2.newBuilder().setOInt(1).addRString("foo").build();
+ TestMessage2 message2 = TestMessage2.newBuilder().setOInt(2).addRString("bar").build();
+ ImmutableMap<String, Object> altActualObjects = mapOf("a", (Object) "Foo!", "b", 42);
+
+ assertThat(altActualObjects).containsExactly("a", "Foo! Bar!".substring(0, 4), "b", 21 * 2);
+ assertThat(altActualObjects).doesNotContainEntry("a", message1);
+ assertThat(altActualObjects).doesNotContainEntry("b", message2);
+ }
+
+ @Test
+ public void testMultimapOverloads_assertAbout() {
+ TestMessage2 message1 = parse("o_int: 1 r_string: \"foo\"");
+ TestMessage2 message2 = parse("o_int: 2 r_string: \"bar\"");
+ TestMessage2 eqMessage2 = parse("o_int: 2 r_string: \"bar\"");
+
+ assertAbout(protos())
+ .that(multimapOf(1, message1, 1, message2, 2, message1))
+ .containsEntry(1, eqMessage2);
+ }
+
+ @Test
+ public void testMultimapOverloads_assertAbout_listAndSet() {
+ TestMessage2 message1 = parse("o_int: 1 r_string: \"foo\"");
+ TestMessage2 message2 = parse("o_int: 2 r_string: \"bar\"");
+ ImmutableMultimap<Integer, TestMessage2> multimap =
+ multimapOf(1, message1, 1, message2, 2, message1);
+ ImmutableListMultimap<Integer, TestMessage2> listMultimap =
+ ImmutableListMultimap.copyOf(multimap);
+ ImmutableSetMultimap<Integer, TestMessage2> setMultimap = ImmutableSetMultimap.copyOf(multimap);
+
+ assertAbout(protos())
+ .that(multimap)
+ .ignoringRepeatedFieldOrderForValues()
+ .containsExactlyEntriesIn(listMultimap);
+ assertAbout(protos())
+ .that(listMultimap)
+ .ignoringRepeatedFieldOrderForValues()
+ .containsExactlyEntriesIn(setMultimap);
+ assertAbout(protos())
+ .that(setMultimap)
+ .ignoringRepeatedFieldOrderForValues()
+ .containsExactlyEntriesIn(multimap);
+ assertAbout(protos()).that(listMultimap).isNotEqualTo(setMultimap);
+ }
+
+ @Test
+ public void testMultimapOverloads_testMessages_normalMethods() {
+ TestMessage2 message1 = parse("o_int: 1 r_string: \"foo\"");
+ TestMessage2 message2 = parse("o_int: 2 r_string: \"bar\"");
+ TestMessage2 eqMessage2 = parse("o_int: 2 r_string: \"bar\"");
+ Object object1 = message1;
+ Object object2 = message2;
+ ImmutableMultimap<Integer, TestMessage2> actualMessages =
+ multimapOf(1, message1, 1, message2, 2, message1);
+
+ assertThat(actualMessages)
+ .containsExactlyEntriesIn(multimapOf(1, message1, 1, eqMessage2, 2, message1))
+ .inOrder();
+ assertThat(actualMessages).hasSize(3);
+ assertThat(actualMessages).isEqualTo(multimapOf(2, message1, 1, message1, 1, eqMessage2));
+ assertThat(actualMessages).isNotEqualTo(multimapOf(2, object1, 1, object2, 1, object1));
+ }
+
+ @Test
+ public void testMultimapOverloads_testMessages_normalMethods_listAndSet() {
+ TestMessage2 message1 = parse("o_int: 1 r_string: \"foo\"");
+ TestMessage2 message2 = parse("o_int: 2 r_string: \"bar\"");
+ TestMessage2 eqMessage2 = parse("o_int: 2 r_string: \"bar\"");
+ ImmutableMultimap<Integer, TestMessage2> multimap =
+ multimapOf(1, message1, 1, message2, 2, message1);
+ ImmutableListMultimap<Integer, TestMessage2> listMultimap =
+ ImmutableListMultimap.copyOf(multimap);
+ ImmutableSetMultimap<Integer, TestMessage2> setMultimap = ImmutableSetMultimap.copyOf(multimap);
+
+ assertThat(multimap)
+ .containsExactlyEntriesIn(multimapOf(1, message1, 1, eqMessage2, 2, message1))
+ .inOrder();
+ assertThat(listMultimap)
+ .containsExactlyEntriesIn(multimapOf(1, message1, 1, eqMessage2, 2, message1))
+ .inOrder();
+ assertThat(setMultimap)
+ .containsExactlyEntriesIn(multimapOf(1, message1, 1, eqMessage2, 2, message1))
+ .inOrder();
+ assertThat(multimap).hasSize(3);
+ assertThat(listMultimap).hasSize(3);
+ assertThat(setMultimap).hasSize(3);
+ }
+
+ @Test
+ public void testMultimapOverloads_testMessages_specializedMethods() {
+ TestMessage2 message1 = parse("o_int: 1 r_string: \"foo\"");
+ TestMessage2 message2 = parse("o_int: 2 r_string: \"bar\"");
+ TestMessage2 message3 = parse("o_int: 3 r_string: \"baz\"");
+ TestMessage2 message4 = parse("o_int: 4 r_string: \"qux\"");
+ ImmutableMultimap<Integer, TestMessage2> actualMessages =
+ multimapOf(1, message1, 1, message2, 2, message1);
+
+ assertThat(actualMessages)
+ .ignoringFieldsForValues(getFieldNumber("o_int"), getFieldNumber("r_string"))
+ .containsExactlyEntriesIn(multimapOf(1, message3, 1, message4, 2, message3))
+ .inOrder();
+ assertThat(actualMessages)
+ .valuesForKey(1)
+ .ignoringFields(getFieldNumber("o_int"), getFieldNumber("r_string"))
+ .containsExactly(message3, message4)
+ .inOrder();
+ }
+
+ @Test
+ public void testMultimapOverloads_objects_actuallyMessages() {
+ TestMessage2 message1 = parse("o_int: 1 r_string: \"foo\"");
+ TestMessage2 message2 = parse("o_int: 2 r_string: \"bar\"");
+ TestMessage2 eqMessage2 = parse("o_int: 2 r_string: \"bar\"");
+ Object object1 = message1;
+ Object object2 = message2;
+ ImmutableMultimap<String, Object> actualObjects =
+ multimapOf("a", object1, "a", object2, "b", object1);
+
+ assertThat(actualObjects)
+ .containsExactlyEntriesIn(multimapOf("a", message1, "a", message2, "b", message1))
+ .inOrder();
+ assertThat(actualObjects).hasSize(3);
+ assertThat(actualObjects).containsEntry("a", eqMessage2);
+ }
+
+ @Test
+ public void testMultimapOverloads_objects_actuallyNotMessages() {
+ TestMessage2 message1 = TestMessage2.newBuilder().setOInt(1).addRString("foo").build();
+ TestMessage2 message2 = TestMessage2.newBuilder().setOInt(2).addRString("bar").build();
+ ImmutableMultimap<String, Object> altActualObjects =
+ multimapOf("a", (Object) "Foo!", "a", "Baz!", "b", 42);
+
+ assertThat(altActualObjects)
+ .containsExactlyEntriesIn(
+ multimapOf("b", 21 * 2, "a", "Ba" + "z!", "a", "Foo! Bar!".substring(0, 4)));
+ assertThat(altActualObjects).doesNotContainEntry("a", message1);
+ assertThat(altActualObjects).doesNotContainEntry("b", message2);
+ }
+}
diff --git a/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/ProtoSubjectTest.java b/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/ProtoSubjectTest.java
new file mode 100644
index 00000000..9b3c0a68
--- /dev/null
+++ b/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/ProtoSubjectTest.java
@@ -0,0 +1,881 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.protobuf.Any;
+import com.google.protobuf.Descriptors.Descriptor;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.DynamicMessage;
+import com.google.protobuf.ExtensionRegistry;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.Message;
+import com.google.protobuf.UnknownFieldSet;
+import java.util.Collection;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/** Unit tests for {@link ProtoSubject}. */
+@RunWith(Parameterized.class)
+public class ProtoSubjectTest extends ProtoSubjectTestBase {
+
+ @Parameters(name = "{0}")
+ public static Collection<Object[]> parameters() {
+ return ProtoSubjectTestBase.parameters();
+ }
+
+ public ProtoSubjectTest(TestType testType) {
+ super(testType);
+ }
+
+ @Test
+ public void testDifferentClasses() throws InvalidProtocolBufferException {
+ Message message = parse("o_int: 3");
+ DynamicMessage dynamicMessage =
+ DynamicMessage.parseFrom(message.getDescriptorForType(), message.toByteString());
+
+ expectThat(message).isEqualTo(dynamicMessage);
+ expectThat(dynamicMessage).isEqualTo(message);
+ }
+
+ @Test
+ public void testDifferentDynamicDescriptors() throws InvalidProtocolBufferException {
+ // Only test once.
+ if (!isProto3()) {
+ return;
+ }
+
+ DynamicMessage message1 =
+ DynamicMessage.parseFrom(
+ TestMessage2.getDescriptor(),
+ TestMessage2.newBuilder().setOInt(43).build().toByteString());
+ DynamicMessage message2 =
+ DynamicMessage.parseFrom(
+ TestMessage3.getDescriptor(),
+ TestMessage3.newBuilder().setOInt(43).build().toByteString());
+
+ expectFailureWhenTesting().that(message1).isEqualTo(message2);
+ expectThatFailure().hasMessageThat().contains("different descriptors");
+ }
+
+ @Test
+ public void testFullDiffOnlyWhenRelevant() {
+ // There are no matches, so 'Full diff' should not be printed.
+ expectFailureWhenTesting().that(parse("o_int: 3")).isEqualTo(parse("o_int: 4"));
+ expectThatFailure().hasMessageThat().doesNotContain("Full diff");
+
+ // r_string is matched, so the 'Full diff' contains extra information.
+ expectFailureWhenTesting()
+ .that(parse("o_int: 3 r_string: 'abc'"))
+ .isEqualTo(parse("o_int: 4 r_string: 'abc'"));
+ expectThatFailure().hasMessageThat().contains("Full diff");
+ }
+
+ @Test
+ public void testIgnoringFieldAbsence() {
+ Message message = parse("o_int: 3");
+ Message diffMessage = parse("o_int: 3 o_enum: DEFAULT");
+
+ // Make sure the implementation is reflexive.
+ if (isProto3()) {
+ expectThat(diffMessage).isEqualTo(message);
+ expectThat(message).isEqualTo(diffMessage);
+ } else {
+ expectThat(diffMessage).isNotEqualTo(message);
+ expectThat(message).isNotEqualTo(diffMessage);
+ }
+ expectThat(diffMessage).ignoringFieldAbsence().isEqualTo(message);
+ expectThat(message).ignoringFieldAbsence().isEqualTo(diffMessage);
+
+ if (!isProto3()) {
+ Message customDefaultMessage = parse("o_int: 3");
+ Message diffCustomDefaultMessage = parse("o_int: 3 o_long_defaults_to_42: 42");
+
+ expectThat(diffCustomDefaultMessage).isNotEqualTo(customDefaultMessage);
+ expectThat(diffCustomDefaultMessage).ignoringFieldAbsence().isEqualTo(customDefaultMessage);
+ expectThat(customDefaultMessage).isNotEqualTo(diffCustomDefaultMessage);
+ expectThat(customDefaultMessage).ignoringFieldAbsence().isEqualTo(diffCustomDefaultMessage);
+ }
+
+ if (!isProto3()) {
+ expectFailureWhenTesting().that(diffMessage).isEqualTo(message);
+ expectIsEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("added: o_enum: DEFAULT");
+ }
+
+ expectFailureWhenTesting().that(diffMessage).ignoringFieldAbsence().isNotEqualTo(message);
+ expectIsNotEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("matched: o_int: 3");
+ if (!isProto3()) {
+ // Proto 3 doesn't cover the field at all when it's not set.
+ expectThatFailure().hasMessageThat().contains("matched: o_enum: DEFAULT");
+ }
+ }
+
+ @Test
+ public void testIgnoringFieldAbsence_anyMessage() {
+ Message message = parse("o_int: 3");
+ Message diffMessage = parse("o_int: 3 o_any_message: {}");
+
+ expectThat(diffMessage).ignoringFieldAbsence().isEqualTo(message);
+ expectThat(message).ignoringFieldAbsence().isEqualTo(diffMessage);
+
+ expectFailureWhenTesting().that(diffMessage).isEqualTo(message);
+ expectIsEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("added: o_any_message:");
+
+ expectFailureWhenTesting().that(diffMessage).ignoringFieldAbsence().isNotEqualTo(message);
+ expectIsNotEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("matched: o_int: 3");
+ expectThatFailure().hasMessageThat().contains("matched: o_any_message");
+ }
+
+ @Test
+ public void testIgnoringFieldAbsence_scoped() {
+ Message message = parse("o_sub_test_message: { o_test_message: {} }");
+ Message emptyMessage = parse("");
+ Message partialMessage = parse("o_sub_test_message: {}");
+
+ // All three are equal if we ignore field absence entirely.
+ expectThat(emptyMessage).ignoringFieldAbsence().isEqualTo(message);
+ expectThat(partialMessage).ignoringFieldAbsence().isEqualTo(message);
+
+ // If we ignore only o_sub_test_message.o_test_message, only the partial message is equal.
+ FieldDescriptor subTestMessageField = getFieldDescriptor("o_sub_test_message");
+ FieldDescriptor subTestMessageTestMessageField =
+ checkNotNull(subTestMessageField.getMessageType().findFieldByName("o_test_message"));
+ expectThat(partialMessage)
+ .ignoringFieldAbsenceOfFieldDescriptors(subTestMessageTestMessageField)
+ .isEqualTo(message);
+ expectFailureWhenTesting()
+ .that(emptyMessage)
+ .ignoringFieldAbsenceOfFieldDescriptors(subTestMessageTestMessageField)
+ .isEqualTo(message);
+ expectIsEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("deleted: o_sub_test_message");
+
+ // But, we can ignore both.
+ expectThat(partialMessage)
+ .ignoringFieldAbsenceOfFieldDescriptors(subTestMessageField)
+ .ignoringFieldAbsenceOfFieldDescriptors(subTestMessageTestMessageField)
+ .isEqualTo(message);
+ expectThat(partialMessage)
+ .ignoringFieldAbsenceOfFieldDescriptors(subTestMessageField, subTestMessageTestMessageField)
+ .isEqualTo(message);
+ expectThat(emptyMessage)
+ .ignoringFieldAbsenceOfFieldDescriptors(subTestMessageField)
+ .ignoringFieldAbsenceOfFieldDescriptors(subTestMessageTestMessageField)
+ .isEqualTo(message);
+ expectThat(emptyMessage)
+ .ignoringFieldAbsenceOfFieldDescriptors(subTestMessageField, subTestMessageTestMessageField)
+ .isEqualTo(message);
+
+ try {
+ expectThat(message)
+ .ignoringFieldAbsenceOfFieldDescriptors(getFieldDescriptor("r_string"))
+ .isEqualTo(message);
+ fail("Expected failure.");
+ } catch (Exception e) {
+ assertThat(e).hasMessageThat().contains("r_string");
+ assertThat(e).hasMessageThat().contains("repeated fields cannot be absent");
+ }
+
+ if (isProto3()) {
+ try {
+ expectThat(message)
+ .ignoringFieldAbsenceOfFieldDescriptors(getFieldDescriptor("o_double"))
+ .isEqualTo(message);
+ fail("Expected failure.");
+ } catch (Exception e) {
+ assertThat(e).hasMessageThat().contains("o_double");
+ assertThat(e).hasMessageThat().contains("is a primitive field in a Proto 3 message");
+ }
+ } else {
+ expectThat(message)
+ .ignoringFieldAbsenceOfFieldDescriptors(getFieldDescriptor("o_double"))
+ .isEqualTo(message);
+ }
+ }
+
+ @Test
+ public void testUnknownFields() throws InvalidProtocolBufferException {
+ Message message =
+ fromUnknownFields(
+ UnknownFieldSet.newBuilder()
+ .addField(99, UnknownFieldSet.Field.newBuilder().addVarint(42).build())
+ .build());
+ Message diffMessage =
+ fromUnknownFields(
+ UnknownFieldSet.newBuilder()
+ .addField(93, UnknownFieldSet.Field.newBuilder().addVarint(42).build())
+ .build());
+
+ expectThat(diffMessage).isNotEqualTo(message);
+ expectThat(diffMessage).ignoringFieldAbsence().isEqualTo(message);
+
+ expectFailureWhenTesting().that(diffMessage).isEqualTo(message);
+ expectIsEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("added: 93[0]: 42");
+ expectThatFailure().hasMessageThat().contains("deleted: 99[0]: 42");
+
+ expectFailureWhenTesting().that(diffMessage).ignoringFieldAbsence().isNotEqualTo(message);
+ expectIsNotEqualToFailed();
+ }
+
+ @Test
+ public void testRepeatedFieldOrder() {
+ Message message = parse("r_string: \"foo\" r_string: \"bar\"");
+ Message eqMessage = parse("r_string: \"bar\" r_string: \"foo\"");
+ Message diffMessage = parse("r_string: \"foo\" r_string: \"foo\" r_string: \"bar\"");
+
+ expectThat(message).isEqualTo(message.toBuilder().build());
+ expectThat(message).ignoringRepeatedFieldOrder().isEqualTo(message.toBuilder().build());
+ expectThat(diffMessage).isNotEqualTo(message);
+ expectThat(diffMessage).ignoringRepeatedFieldOrder().isNotEqualTo(message);
+ expectThat(eqMessage).isNotEqualTo(message);
+ expectThat(eqMessage).ignoringRepeatedFieldOrder().isEqualTo(message);
+
+ Message nestedMessage =
+ parse(
+ "r_test_message: { o_int: 33 r_string: \"foo\" r_string: \"bar\" } "
+ + "r_test_message: { o_int: 44 r_string: \"baz\" r_string: \"qux\" } ");
+ Message diffNestedMessage =
+ parse(
+ "r_test_message: { o_int: 33 r_string: \"qux\" r_string: \"baz\" } "
+ + "r_test_message: { o_int: 44 r_string: \"bar\" r_string: \"foo\" } ");
+ Message eqNestedMessage =
+ parse(
+ "r_test_message: { o_int: 44 r_string: \"qux\" r_string: \"baz\" } "
+ + "r_test_message: { o_int: 33 r_string: \"bar\" r_string: \"foo\" } ");
+
+ expectThat(nestedMessage).isEqualTo(nestedMessage.toBuilder().build());
+ expectThat(nestedMessage)
+ .ignoringRepeatedFieldOrder()
+ .isEqualTo(nestedMessage.toBuilder().build());
+ expectThat(diffNestedMessage).isNotEqualTo(nestedMessage);
+ expectThat(diffNestedMessage).ignoringRepeatedFieldOrder().isNotEqualTo(nestedMessage);
+ expectThat(eqNestedMessage).isNotEqualTo(nestedMessage);
+ expectThat(eqNestedMessage).ignoringRepeatedFieldOrder().isEqualTo(nestedMessage);
+
+ expectFailureWhenTesting().that(eqMessage).isEqualTo(message);
+ expectIsEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("modified: r_string[0]: \"foo\" -> \"bar\"");
+ expectThatFailure().hasMessageThat().contains("modified: r_string[1]: \"bar\" -> \"foo\"");
+
+ expectFailureWhenTesting().that(eqMessage).ignoringRepeatedFieldOrder().isNotEqualTo(message);
+ expectIsNotEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("moved: r_string[0] -> r_string[1]: \"foo\"");
+ expectThatFailure().hasMessageThat().contains("moved: r_string[1] -> r_string[0]: \"bar\"");
+
+ expectFailureWhenTesting().that(diffMessage).ignoringRepeatedFieldOrder().isEqualTo(message);
+ expectIsEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("matched: r_string[0]: \"foo\"");
+ expectThatFailure().hasMessageThat().contains("moved: r_string[1] -> r_string[2]: \"bar\"");
+ expectThatFailure().hasMessageThat().contains("added: r_string[1]: \"foo\"");
+ }
+
+ @Test
+ public void testRepeatedFieldOrder_scoped() {
+ Message message =
+ parse("r_string: 'a' r_string: 'b' o_sub_test_message: { r_string: 'c' r_string: 'd' }");
+ Message diffSubMessage =
+ parse("r_string: 'a' r_string: 'b' o_sub_test_message: { r_string: 'd' r_string: 'c' }");
+ Message diffAll =
+ parse("r_string: 'b' r_string: 'a' o_sub_test_message: { r_string: 'd' r_string: 'c' }");
+
+ FieldDescriptor rootMessageRepeatedfield = getFieldDescriptor("r_string");
+ FieldDescriptor subMessageRepeatedField =
+ checkNotNull(
+ getFieldDescriptor("o_sub_test_message").getMessageType().findFieldByName("r_string"));
+
+ // Ignoring all repeated field order tests pass.
+ expectThat(diffSubMessage).ignoringRepeatedFieldOrder().isEqualTo(message);
+ expectThat(diffAll).ignoringRepeatedFieldOrder().isEqualTo(message);
+
+ // Ignoring only some results in failures.
+ //
+ // TODO(user): Whether we check failure message substrings or not is currently ad-hoc on a
+ // per-test basis, and not especially maintainable. We should make the tests consistent
+ // according to some reasonable rule in this regard.
+ expectFailureWhenTesting()
+ .that(diffSubMessage)
+ .ignoringRepeatedFieldOrderOfFieldDescriptors(rootMessageRepeatedfield)
+ .isEqualTo(message);
+ expectIsEqualToFailed();
+ expectThatFailure()
+ .hasMessageThat()
+ .contains("modified: o_sub_test_message.r_string[0]: \"c\" -> \"d\"");
+ expectThat(diffSubMessage)
+ .ignoringRepeatedFieldOrderOfFieldDescriptors(subMessageRepeatedField)
+ .isEqualTo(message);
+
+ expectThat(diffAll)
+ .ignoringRepeatedFieldOrderOfFieldDescriptors(rootMessageRepeatedfield)
+ .isNotEqualTo(message);
+ expectThat(diffAll)
+ .ignoringRepeatedFieldOrderOfFieldDescriptors(subMessageRepeatedField)
+ .isNotEqualTo(message);
+ expectThat(diffAll)
+ .ignoringRepeatedFieldOrderOfFieldDescriptors(
+ rootMessageRepeatedfield, subMessageRepeatedField)
+ .isEqualTo(message);
+
+ try {
+ expectThat(message)
+ .ignoringRepeatedFieldOrderOfFields(getFieldNumber("o_int"))
+ .isEqualTo(message);
+ fail("Expected failure.");
+ } catch (Exception e) {
+ assertThat(e).hasMessageThat().contains("o_int");
+ assertThat(e).hasMessageThat().contains("is not a repeated field");
+ }
+ }
+
+ @Test
+ public void testDoubleTolerance() {
+ Message message = parse("o_double: 1.0");
+ Message diffMessage = parse("o_double: 1.1");
+
+ expectThat(diffMessage).isNotEqualTo(message);
+ expectThat(diffMessage).usingDoubleTolerance(0.2).isEqualTo(message);
+ expectThat(diffMessage).usingDoubleTolerance(0.05).isNotEqualTo(message);
+ expectThat(diffMessage).usingFloatTolerance(0.2f).isNotEqualTo(message);
+ }
+
+ @Test
+ public void testDoubleTolerance_defaultValue() {
+ Message message = parse("o_double: 0.0");
+ Message defaultInstance = parse("");
+ Message diffMessage = parse("o_double: 0.01");
+
+ expectThat(diffMessage).isNotEqualTo(message);
+
+ if (isProto3()) {
+ // The default value is ignored and treated as unset.
+ // We treat is as equivalent to being set to 0.0 because there is no distinction in Proto 3.
+ expectThat(message).isEqualTo(defaultInstance);
+ expectThat(diffMessage).usingDoubleTolerance(0.1).isEqualTo(message);
+ expectThat(diffMessage).usingDoubleTolerance(0.1).ignoringFieldAbsence().isEqualTo(message);
+ } else {
+ // The default value can be set or unset, so we respect it.
+ expectThat(message).isNotEqualTo(defaultInstance);
+
+ expectThat(diffMessage).usingDoubleTolerance(0.1).isEqualTo(message);
+ expectThat(diffMessage).usingDoubleTolerance(0.1).isNotEqualTo(defaultInstance);
+ expectThat(diffMessage).usingDoubleTolerance(0.1).ignoringFieldAbsence().isEqualTo(message);
+ expectThat(diffMessage)
+ .usingDoubleTolerance(0.1)
+ .ignoringFieldAbsence()
+ .isEqualTo(defaultInstance);
+
+ // The same logic applies for a specified default; not available in Proto 3.
+ Message message2 = parse("o_double_defaults_to_42: 42.0");
+ Message diffMessage2 = parse("o_double_defaults_to_42: 42.01");
+
+ expectThat(diffMessage2).usingDoubleTolerance(0.1).isEqualTo(message2);
+ expectThat(diffMessage2).usingDoubleTolerance(0.1).isNotEqualTo(defaultInstance);
+ expectThat(diffMessage2).usingDoubleTolerance(0.1).ignoringFieldAbsence().isEqualTo(message2);
+ expectThat(diffMessage2)
+ .usingDoubleTolerance(0.1)
+ .ignoringFieldAbsence()
+ .isEqualTo(defaultInstance);
+ }
+ }
+
+ @Test
+ public void testDoubleTolerance_scoped() {
+ Message message = parse("o_double: 1.0 o_double2: 1.0");
+ Message diffMessage = parse("o_double: 1.1 o_double2: 1.5");
+
+ int doubleFieldNumber = getFieldNumber("o_double");
+ int double2FieldNumber = getFieldNumber("o_double2");
+
+ expectThat(diffMessage).usingDoubleTolerance(0.6).isEqualTo(message);
+ expectThat(diffMessage).usingDoubleTolerance(0.2).isNotEqualTo(message);
+
+ // usingDoubleTolerance*() statements override all previous statements.
+ expectThat(diffMessage)
+ .usingDoubleTolerance(0.2)
+ .usingDoubleToleranceForFields(0.6, double2FieldNumber)
+ .isEqualTo(message);
+ expectThat(diffMessage)
+ .usingDoubleToleranceForFields(0.6, doubleFieldNumber, double2FieldNumber)
+ .usingDoubleToleranceForFields(0.2, doubleFieldNumber)
+ .isEqualTo(message);
+
+ expectThat(diffMessage)
+ .usingDoubleTolerance(0.2)
+ .usingDoubleToleranceForFields(0.6, doubleFieldNumber)
+ .isNotEqualTo(message);
+ expectThat(diffMessage)
+ .usingDoubleToleranceForFields(0.6, double2FieldNumber)
+ .usingDoubleTolerance(0.2)
+ .isNotEqualTo(message);
+ expectThat(diffMessage)
+ .usingDoubleToleranceForFields(0.6, doubleFieldNumber, double2FieldNumber)
+ .usingDoubleToleranceForFields(0.2, double2FieldNumber)
+ .isNotEqualTo(message);
+
+ try {
+ expectThat(message)
+ .usingDoubleToleranceForFields(3.14159, getFieldNumber("o_int"))
+ .isEqualTo(message);
+ fail("Expected failure.");
+ } catch (Exception e) {
+ assertThat(e).hasMessageThat().contains("o_int");
+ assertThat(e).hasMessageThat().contains("is not a double field");
+ }
+ }
+
+ @Test
+ public void testFloatTolerance() {
+ Message message = parse("o_float: 1.0");
+ Message diffMessage = parse("o_float: 1.1");
+
+ expectThat(diffMessage).isNotEqualTo(message);
+ expectThat(diffMessage).usingFloatTolerance(0.2f).isEqualTo(message);
+ expectThat(diffMessage).usingFloatTolerance(0.05f).isNotEqualTo(message);
+ expectThat(diffMessage).usingDoubleTolerance(0.2).isNotEqualTo(message);
+ }
+
+ @Test
+ public void testFloatTolerance_defaultValue() {
+ Message message = parse("o_float: 0.0");
+ Message defaultInstance = parse("");
+ Message diffMessage = parse("o_float: 0.01");
+
+ expectThat(diffMessage).isNotEqualTo(message);
+
+ if (isProto3()) {
+ // The default value is ignored and treated as unset.
+ // We treat is as equivalent to being set to 0.0f because there is no distinction in Proto 3.
+ expectThat(message).isEqualTo(defaultInstance);
+ expectThat(diffMessage).usingFloatTolerance(0.1f).isEqualTo(message);
+ expectThat(diffMessage).usingFloatTolerance(0.1f).ignoringFieldAbsence().isEqualTo(message);
+ } else {
+ // The default value can be set or unset, so we respect it.
+ expectThat(message).isNotEqualTo(defaultInstance);
+
+ expectThat(diffMessage).usingFloatTolerance(0.1f).isEqualTo(message);
+ expectThat(diffMessage).usingFloatTolerance(0.1f).isNotEqualTo(defaultInstance);
+ expectThat(diffMessage).usingFloatTolerance(0.1f).ignoringFieldAbsence().isEqualTo(message);
+ expectThat(diffMessage)
+ .usingFloatTolerance(0.1f)
+ .ignoringFieldAbsence()
+ .isEqualTo(defaultInstance);
+
+ // The same logic applies for a specified default; not available in Proto 3.
+ Message message2 = parse("o_float_defaults_to_42: 42.0");
+ Message diffMessage2 = parse("o_float_defaults_to_42: 42.01");
+
+ expectThat(diffMessage2).usingFloatTolerance(0.1f).isEqualTo(message2);
+ expectThat(diffMessage2).usingFloatTolerance(0.1f).isNotEqualTo(defaultInstance);
+ expectThat(diffMessage2).usingFloatTolerance(0.1f).ignoringFieldAbsence().isEqualTo(message2);
+ expectThat(diffMessage2)
+ .usingFloatTolerance(0.1f)
+ .ignoringFieldAbsence()
+ .isEqualTo(defaultInstance);
+ }
+ }
+
+ @Test
+ public void testFloatTolerance_scoped() {
+ Message message = parse("o_float: 1.0 o_float2: 1.0");
+ Message diffMessage = parse("o_float: 1.1 o_float2: 1.5");
+
+ int floatFieldNumber = getFieldNumber("o_float");
+ int float2FieldNumber = getFieldNumber("o_float2");
+
+ expectThat(diffMessage).usingFloatTolerance(0.6f).isEqualTo(message);
+ expectThat(diffMessage).usingFloatTolerance(0.2f).isNotEqualTo(message);
+
+ // usingFloatTolerance*() statements override all previous statements.
+ expectThat(diffMessage)
+ .usingFloatTolerance(0.2f)
+ .usingFloatToleranceForFields(0.6f, float2FieldNumber)
+ .isEqualTo(message);
+ expectThat(diffMessage)
+ .usingFloatTolerance(0.2f)
+ .usingFloatToleranceForFields(0.6f, floatFieldNumber)
+ .isNotEqualTo(message);
+ expectThat(diffMessage)
+ .usingFloatToleranceForFields(0.6f, float2FieldNumber)
+ .usingFloatTolerance(0.2f)
+ .isNotEqualTo(message);
+ expectThat(diffMessage)
+ .usingFloatToleranceForFields(0.6f, floatFieldNumber, float2FieldNumber)
+ .usingFloatToleranceForFields(0.2f, floatFieldNumber)
+ .isEqualTo(message);
+ expectThat(diffMessage)
+ .usingFloatToleranceForFields(0.6f, floatFieldNumber, float2FieldNumber)
+ .usingFloatToleranceForFields(0.2f, float2FieldNumber)
+ .isNotEqualTo(message);
+
+ try {
+ expectThat(message)
+ .usingFloatToleranceForFields(3.1416f, getFieldNumber("o_int"))
+ .isEqualTo(message);
+ fail("Expected failure.");
+ } catch (Exception e) {
+ assertThat(e).hasMessageThat().contains("o_int");
+ assertThat(e).hasMessageThat().contains("is not a float field");
+ }
+ }
+
+ @Test
+ public void testComparingExpectedFieldsOnly() {
+ Message message = parse("o_int: 3 r_string: 'foo'");
+ Message narrowMessage = parse("o_int: 3");
+
+ expectThat(message).comparingExpectedFieldsOnly().isEqualTo(narrowMessage);
+ expectThat(narrowMessage).comparingExpectedFieldsOnly().isNotEqualTo(message);
+
+ expectFailureWhenTesting()
+ .that(message)
+ .comparingExpectedFieldsOnly()
+ .isNotEqualTo(narrowMessage);
+ expectThatFailure().hasMessageThat().contains("ignored: r_string");
+ }
+
+ @Test
+ public void testIgnoringExtraRepeatedFieldElements_respectingOrder() {
+ Message message = parse("r_string: 'foo' r_string: 'bar'");
+ Message eqMessage = parse("r_string: 'foo' r_string: 'foobar' r_string: 'bar'");
+ Message diffMessage = parse("r_string: 'bar' r_string: 'foobar' r_string: 'foo'");
+
+ expectThat(eqMessage).ignoringExtraRepeatedFieldElements().isEqualTo(message);
+ expectThat(diffMessage).ignoringExtraRepeatedFieldElements().isNotEqualTo(message);
+
+ expectFailureWhenTesting()
+ .that(eqMessage)
+ .ignoringExtraRepeatedFieldElements()
+ .isNotEqualTo(message);
+ expectThatFailure()
+ .hasMessageThat()
+ .contains("ignored: r_string[?] -> r_string[1]: \"foobar\"");
+
+ expectFailureWhenTesting()
+ .that(diffMessage)
+ .ignoringExtraRepeatedFieldElements()
+ .isEqualTo(message);
+ expectThatFailure()
+ .hasMessageThat()
+ .contains("out_of_order: r_string[1] -> r_string[0]: \"bar\"");
+ expectThatFailure().hasMessageThat().contains("moved: r_string[0] -> r_string[2]: \"foo\"");
+ }
+
+ @Test
+ public void testIgnoringExtraRepeatedFieldElements_ignoringOrder() {
+ Message message = parse("r_string: 'foo' r_string: 'bar'");
+ Message eqMessage = parse("r_string: 'baz' r_string: 'bar' r_string: 'qux' r_string: 'foo'");
+ Message diffMessage = parse("r_string: 'abc' r_string: 'foo' r_string: 'xyz'");
+
+ expectThat(eqMessage)
+ .ignoringExtraRepeatedFieldElements()
+ .ignoringRepeatedFieldOrder()
+ .isEqualTo(message);
+ expectThat(diffMessage)
+ .ignoringExtraRepeatedFieldElements()
+ .ignoringRepeatedFieldOrder()
+ .isNotEqualTo(message);
+
+ expectFailureWhenTesting()
+ .that(diffMessage)
+ .ignoringExtraRepeatedFieldElements()
+ .ignoringRepeatedFieldOrder()
+ .isEqualTo(message);
+ expectThatFailure().hasMessageThat().contains("moved: r_string[0] -> r_string[1]: \"foo\"");
+ expectThatFailure().hasMessageThat().contains("deleted: r_string[1]: \"bar\"");
+ }
+
+ @Test
+ public void testIgnoringExtraRepeatedFieldElements_empty() {
+ Message message = parse("o_int: 2");
+ Message diffMessage = parse("o_int: 2 r_string: 'error'");
+
+ expectThat(diffMessage).ignoringExtraRepeatedFieldElements().isNotEqualTo(message);
+ expectThat(diffMessage)
+ .ignoringExtraRepeatedFieldElements()
+ .ignoringRepeatedFieldOrder()
+ .isNotEqualTo(message);
+
+ expectThat(diffMessage)
+ .comparingExpectedFieldsOnly()
+ .ignoringExtraRepeatedFieldElements()
+ .isEqualTo(message);
+ expectThat(diffMessage)
+ .comparingExpectedFieldsOnly()
+ .ignoringExtraRepeatedFieldElements()
+ .ignoringRepeatedFieldOrder()
+ .isEqualTo(message);
+ }
+
+ @Test
+ public void testIgnoringExtraRepeatedFieldElements_scoped() {
+ Message message = parse("r_string: 'a' o_sub_test_message: { r_string: 'c' }");
+ Message diffMessage =
+ parse("r_string: 'a' o_sub_test_message: { r_string: 'b' r_string: 'c' }");
+
+ FieldDescriptor rootRepeatedField = getFieldDescriptor("r_string");
+ FieldDescriptor subMessageRepeatedField =
+ checkNotNull(
+ getFieldDescriptor("o_sub_test_message").getMessageType().findFieldByName("r_string"));
+
+ expectThat(diffMessage).ignoringExtraRepeatedFieldElements().isEqualTo(message);
+ expectThat(diffMessage)
+ .ignoringExtraRepeatedFieldElementsOfFieldDescriptors(rootRepeatedField)
+ .isNotEqualTo(message);
+ expectThat(diffMessage)
+ .ignoringExtraRepeatedFieldElementsOfFieldDescriptors(subMessageRepeatedField)
+ .isEqualTo(message);
+ expectThat(diffMessage)
+ .ignoringExtraRepeatedFieldElementsOfFieldDescriptors(
+ rootRepeatedField, subMessageRepeatedField)
+ .isEqualTo(message);
+
+ try {
+ expectThat(message)
+ .ignoringExtraRepeatedFieldElementsOfFields(getFieldNumber("o_int"))
+ .isEqualTo(message);
+ fail("Expected failure.");
+ } catch (Exception e) {
+ assertThat(e).hasMessageThat().contains("o_int");
+ assertThat(e).hasMessageThat().contains("it cannot contain extra elements");
+ }
+ }
+
+ // Utility which fills a proto map field, based on the java.util.Map.
+ private Message makeProtoMap(Map<String, Integer> map) {
+ StringBuilder textProto = new StringBuilder();
+ for (String key : map.keySet()) {
+ int value = map.get(key);
+ textProto
+ .append("test_message_map { key: '")
+ .append(key)
+ .append("' value { o_int: ")
+ .append(value)
+ .append(" } } ");
+ }
+ return parse(textProto.toString());
+ }
+
+ @Test
+ public void testIgnoringExtraRepeatedFieldElements_map() {
+ Message message = makeProtoMap(ImmutableMap.of("foo", 2, "bar", 3));
+ Message eqMessage = makeProtoMap(ImmutableMap.of("bar", 3, "qux", 4, "foo", 2));
+ Message diffMessage = makeProtoMap(ImmutableMap.of("quz", 5, "foo", 2));
+ Message emptyMessage = parse("");
+
+ expectThat(eqMessage).ignoringExtraRepeatedFieldElements().isEqualTo(message);
+ expectThat(eqMessage)
+ .ignoringRepeatedFieldOrder()
+ .ignoringExtraRepeatedFieldElements()
+ .isEqualTo(message);
+ expectThat(diffMessage).ignoringExtraRepeatedFieldElements().isNotEqualTo(message);
+ expectThat(diffMessage)
+ .ignoringExtraRepeatedFieldElements()
+ .ignoringRepeatedFieldOrder()
+ .isNotEqualTo(message);
+
+ expectThat(message).ignoringExtraRepeatedFieldElements().isNotEqualTo(emptyMessage);
+
+ expectFailureWhenTesting()
+ .that(diffMessage)
+ .ignoringExtraRepeatedFieldElements()
+ .isEqualTo(message);
+ expectThatFailure().hasMessageThat().contains("matched: test_message_map[\"foo\"].o_int: 2");
+ expectThatFailure().hasMessageThat().contains("ignored: test_message_map[\"quz\"]");
+ expectThatFailure().hasMessageThat().contains("deleted: test_message_map[\"bar\"]");
+ }
+
+ @Test
+ public void testReportingMismatchesOnly_isEqualTo() {
+ Message message = parse("r_string: \"foo\" r_string: \"bar\"");
+ Message diffMessage = parse("r_string: \"foo\" r_string: \"not_bar\"");
+
+ expectFailureWhenTesting().that(diffMessage).isEqualTo(message);
+ expectIsEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("foo");
+ expectThatFailure().hasMessageThat().contains("bar");
+ expectThatFailure().hasMessageThat().contains("not_bar");
+
+ expectFailureWhenTesting().that(diffMessage).reportingMismatchesOnly().isEqualTo(message);
+ expectIsEqualToFailed();
+ expectThatFailure().hasMessageThat().doesNotContain("foo");
+ expectThatFailure().hasMessageThat().contains("bar");
+ expectThatFailure().hasMessageThat().contains("not_bar");
+ }
+
+ @Test
+ public void testReportingMismatchesOnly_isNotEqualTo() {
+ Message message = parse("o_int: 33 r_string: \"foo\" r_string: \"bar\"");
+ Message diffMessage = parse("o_int: 33 r_string: \"bar\" r_string: \"foo\"");
+
+ expectFailureWhenTesting().that(diffMessage).ignoringRepeatedFieldOrder().isNotEqualTo(message);
+ expectIsNotEqualToFailed();
+ expectThatFailure().hasMessageThat().contains("33");
+ expectThatFailure().hasMessageThat().contains("foo");
+ expectThatFailure().hasMessageThat().contains("bar");
+
+ expectFailureWhenTesting()
+ .that(diffMessage)
+ .ignoringRepeatedFieldOrder()
+ .reportingMismatchesOnly()
+ .isNotEqualTo(message);
+ expectIsNotEqualToFailed();
+ expectThatFailure().hasMessageThat().doesNotContain("33");
+ expectThatFailure().hasMessageThat().doesNotContain("foo");
+ expectThatFailure().hasMessageThat().doesNotContain("bar");
+ }
+
+ @Test
+ public void testHasAllRequiredFields() {
+ // Proto 3 doesn't have required fields.
+ if (isProto3()) {
+ return;
+ }
+
+ expectThat(parsePartial("")).hasAllRequiredFields();
+ expectThat(parsePartial("o_required_string_message: { required_string: \"foo\" }"))
+ .hasAllRequiredFields();
+
+ expectFailureWhenTesting()
+ .that(parsePartial("o_required_string_message: {}"))
+ .hasAllRequiredFields();
+ expectThatFailure()
+ .factKeys()
+ .containsExactly(
+ "expected to have all required fields set", "but was missing", "proto was");
+ expectThatFailure()
+ .factValue("but was missing")
+ .isEqualTo("[o_required_string_message.required_string]");
+
+ expectFailureWhenTesting()
+ .that(parsePartial("r_required_string_message: {} r_required_string_message: {}"))
+ .hasAllRequiredFields();
+ expectThatFailure()
+ .factKeys()
+ .containsExactly(
+ "expected to have all required fields set", "but was missing", "proto was");
+ expectThatFailure()
+ .factValue("but was missing")
+ .contains("r_required_string_message[0].required_string");
+ expectThatFailure()
+ .factValue("but was missing")
+ .contains("r_required_string_message[1].required_string");
+ }
+
+ @Test
+ public void testAnyMessagesWithDifferentTypes() {
+ String typeUrl =
+ isProto3()
+ ? "type.googleapis.com/com.google.common.truth.extensions.proto.SubTestMessage3"
+ : "type.googleapis.com/com.google.common.truth.extensions.proto.SubTestMessage2";
+ String diffTypeUrl =
+ isProto3()
+ ? "type.googleapis.com/com.google.common.truth.extensions.proto.SubSubTestMessage3"
+ : "type.googleapis.com/com.google.common.truth.extensions.proto.SubSubTestMessage2";
+
+ Message message = parse("o_any_message: { [" + typeUrl + "]: {r_string: \"foo\"} }");
+ Message diffMessage = parse("o_any_message: { [" + diffTypeUrl + "]: {r_string: \"bar\"} }");
+
+ expectThat(message)
+ .unpackingAnyUsing(getTypeRegistry(), getExtensionRegistry())
+ .isNotEqualTo(diffMessage);
+
+ expectFailureWhenTesting()
+ .that(message)
+ .unpackingAnyUsing(getTypeRegistry(), getExtensionRegistry())
+ .isEqualTo(diffMessage);
+ expectThatFailure().hasMessageThat().contains("modified: o_any_message.type_url");
+ expectThatFailure()
+ .hasMessageThat()
+ .containsMatch("modified: o_any_message.value:.*bar.*->.*foo.*");
+ }
+
+ @Test
+ public void testAnyMessageCompareWithEmptyAnyMessage() {
+ String typeUrl =
+ isProto3()
+ ? "type.googleapis.com/com.google.common.truth.extensions.proto.SubTestMessage3"
+ : "type.googleapis.com/com.google.common.truth.extensions.proto.SubTestMessage2";
+
+ Message messageWithAny = parse("o_any_message: { [" + typeUrl + "]: {o_int: 1} }");
+ Message messageWithEmptyAny = parse("o_any_message: { }");
+
+ expectThat(messageWithAny)
+ .unpackingAnyUsing(getTypeRegistry(), getExtensionRegistry())
+ .isNotEqualTo(messageWithEmptyAny);
+ expectThat(messageWithEmptyAny)
+ .unpackingAnyUsing(getTypeRegistry(), getExtensionRegistry())
+ .isNotEqualTo(messageWithAny);
+
+ expectFailureWhenTesting()
+ .that(messageWithAny)
+ .unpackingAnyUsing(getTypeRegistry(), getExtensionRegistry())
+ .isEqualTo(messageWithEmptyAny);
+ expectThatFailure().hasMessageThat().contains("modified: o_any_message.type_url");
+ expectThatFailure().hasMessageThat().contains("modified: o_any_message.value");
+
+ expectFailureWhenTesting()
+ .that(messageWithEmptyAny)
+ .unpackingAnyUsing(getTypeRegistry(), getExtensionRegistry())
+ .isEqualTo(messageWithAny);
+ expectThatFailure().hasMessageThat().contains("modified: o_any_message.type_url");
+ expectThatFailure().hasMessageThat().contains("modified: o_any_message.value");
+ }
+
+ @Test
+ public void testAnyMessageComparedWithDynamicMessage() throws InvalidProtocolBufferException {
+ String typeUrl =
+ isProto3()
+ ? "type.googleapis.com/com.google.common.truth.extensions.proto.SubTestMessage3"
+ : "type.googleapis.com/com.google.common.truth.extensions.proto.SubTestMessage2";
+
+ Message messageWithAny = parse("o_any_message: { [" + typeUrl + "]: {o_int: 1} }");
+ FieldDescriptor fieldDescriptor = getFieldDescriptor("o_any_message");
+ Any message = (Any) messageWithAny.getField(fieldDescriptor);
+ DynamicMessage dynamicMessage =
+ DynamicMessage.parseFrom(
+ Any.getDescriptor(), message.toByteString(), ExtensionRegistry.getEmptyRegistry());
+
+ expectThat(dynamicMessage)
+ .unpackingAnyUsing(getTypeRegistry(), getExtensionRegistry())
+ .isEqualTo(message);
+ expectThat(message)
+ .unpackingAnyUsing(getTypeRegistry(), getExtensionRegistry())
+ .isEqualTo(dynamicMessage);
+ }
+
+ @Test
+ public void testMapWithDefaultKeysAndValues() throws InvalidProtocolBufferException {
+ Descriptor descriptor = getFieldDescriptor("o_int").getContainingType();
+ final String defaultString = "";
+ final int defaultInt32 = 0;
+ Message message = makeProtoMap(ImmutableMap.of(defaultString, 1, "foo", defaultInt32));
+ Message dynamicMessage =
+ DynamicMessage.parseFrom(
+ descriptor, message.toByteString(), ExtensionRegistry.getEmptyRegistry());
+ expectThat(message).isEqualTo(dynamicMessage);
+ }
+}
diff --git a/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/ProtoSubjectTestBase.java b/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/ProtoSubjectTestBase.java
new file mode 100644
index 00000000..a6d4e6b5
--- /dev/null
+++ b/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/ProtoSubjectTestBase.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (c) 2016 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.proto;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.common.truth.TruthFailureSubject.truthFailures;
+import static com.google.protobuf.ExtensionRegistry.getEmptyRegistry;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.common.truth.Expect;
+import com.google.common.truth.ExpectFailure;
+import com.google.common.truth.Subject;
+import com.google.common.truth.TruthFailureSubject;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.ExtensionRegistry;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.Message;
+import com.google.protobuf.TextFormat;
+import com.google.protobuf.TextFormat.ParseException;
+import com.google.protobuf.TypeRegistry;
+import com.google.protobuf.UnknownFieldSet;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.junit.Rule;
+
+/** Base class for testing {@link ProtoSubject} methods. */
+public class ProtoSubjectTestBase {
+
+ // Type information for subclasses.
+ static enum TestType {
+ IMMUTABLE_PROTO2(TestMessage2.getDefaultInstance()),
+ PROTO3(TestMessage3.getDefaultInstance());
+
+ private final Message defaultInstance;
+
+ TestType(Message defaultInstance) {
+ this.defaultInstance = defaultInstance;
+ }
+
+ public Message defaultInstance() {
+ return defaultInstance;
+ }
+
+ public boolean isProto3() {
+ return this == PROTO3;
+ }
+ }
+
+ private static final TypeRegistry typeRegistry =
+ TypeRegistry.newBuilder()
+ .add(TestMessage3.getDescriptor())
+ .add(TestMessage2.getDescriptor())
+ .build();
+
+ private static final TextFormat.Parser PARSER =
+ TextFormat.Parser.newBuilder()
+ .setSingularOverwritePolicy(
+ TextFormat.Parser.SingularOverwritePolicy.FORBID_SINGULAR_OVERWRITES)
+ .setTypeRegistry(typeRegistry)
+ .build();
+
+ private static final ExtensionRegistry extensionRegistry = getEmptyRegistry();
+
+ // For Parameterized testing.
+ protected static Collection<Object[]> parameters() {
+ ImmutableList.Builder<Object[]> builder = ImmutableList.builder();
+ for (TestType testType : TestType.values()) {
+ builder.add(new Object[] {testType});
+ }
+ return builder.build();
+ }
+
+ @Rule public final Expect expect = Expect.create();
+
+ // Hackhackhack: 'ExpectFailure' does not support more than one call per test, but we have many
+ // tests which require it. So, we create an arbitrary number of these rules, and dole them out
+ // in order on demand.
+ // TODO(user): See if 'expectFailure.enterRuleContext()' could be made public, or a '.reset()'
+ // function could be added to mitigate the need for this. Alternatively, if & when Truth moves
+ // to Java 8, we can use the static API with lambdas instead.
+ @Rule public final MultiExpectFailure multiExpectFailure = new MultiExpectFailure(/* size= */ 20);
+
+ private final Message defaultInstance;
+ private final boolean isProto3;
+
+ protected ProtoSubjectTestBase(TestType testType) {
+ this.defaultInstance = testType.defaultInstance();
+ this.isProto3 = testType.isProto3();
+ }
+
+ protected final Message fromUnknownFields(UnknownFieldSet unknownFieldSet)
+ throws InvalidProtocolBufferException {
+ return defaultInstance.getParserForType().parseFrom(unknownFieldSet.toByteArray());
+ }
+
+ protected final String fullMessageName() {
+ return defaultInstance.getDescriptorForType().getFullName();
+ }
+
+ protected final FieldDescriptor getFieldDescriptor(String fieldName) {
+ FieldDescriptor fieldDescriptor =
+ defaultInstance.getDescriptorForType().findFieldByName(fieldName);
+ checkArgument(fieldDescriptor != null, "No field named %s.", fieldName);
+ return fieldDescriptor;
+ }
+
+ protected final int getFieldNumber(String fieldName) {
+ return getFieldDescriptor(fieldName).getNumber();
+ }
+
+ protected final TypeRegistry getTypeRegistry() {
+ return typeRegistry;
+ }
+
+ protected final ExtensionRegistry getExtensionRegistry() {
+ return extensionRegistry;
+ }
+
+ protected Message parse(String textProto) {
+ try {
+ Message.Builder builder = defaultInstance.toBuilder();
+ PARSER.merge(textProto, builder);
+ return builder.build();
+ } catch (ParseException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ protected final Message parsePartial(String textProto) {
+ try {
+ Message.Builder builder = defaultInstance.toBuilder();
+ PARSER.merge(textProto, builder);
+ return builder.buildPartial();
+ } catch (ParseException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ protected final boolean isProto3() {
+ return isProto3;
+ }
+
+ /**
+ * Some tests don't vary across the different proto types, and should only be run once.
+ *
+ * <p>This method returns true for exactly one {@link TestType}, and false for all the others, and
+ * so can be used to ensure tests are only run once.
+ */
+ protected final boolean testIsRunOnce() {
+ return isProto3;
+ }
+
+ protected final ProtoSubjectBuilder expectFailureWhenTesting() {
+ return multiExpectFailure.whenTesting().about(ProtoTruth.protos());
+ }
+
+ protected final TruthFailureSubject expectThatFailure() {
+ return expect.about(truthFailures()).that(multiExpectFailure.getFailure());
+ }
+
+ protected final ProtoSubject expectThat(@Nullable Message message) {
+ return expect.about(ProtoTruth.protos()).that(message);
+ }
+
+ protected final <M extends Message> IterableOfProtosSubject<M> expectThat(Iterable<M> messages) {
+ return expect.about(ProtoTruth.protos()).that(messages);
+ }
+
+ protected final <M extends Message> MapWithProtoValuesSubject<M> expectThat(Map<?, M> map) {
+ return expect.about(ProtoTruth.protos()).that(map);
+ }
+
+ protected final <M extends Message> MultimapWithProtoValuesSubject<M> expectThat(
+ Multimap<?, M> multimap) {
+ return expect.about(ProtoTruth.protos()).that(multimap);
+ }
+
+ protected final ProtoSubject expectThatWithMessage(String msg, @Nullable Message message) {
+ return expect.withMessage(msg).about(ProtoTruth.protos()).that(message);
+ }
+
+ protected final void expectIsEqualToFailed() {
+ expectFailureMatches(
+ "Not true that messages compare equal\\.\\s*"
+ + "(Differences were found:\\n.*|No differences were reported\\..*)");
+ }
+
+ protected final void expectIsNotEqualToFailed() {
+ expectFailureMatches(
+ "Not true that messages compare not equal\\.\\s*"
+ + "(Only ignorable differences were found:\\n.*|"
+ + "No differences were found\\..*)");
+ }
+
+ /**
+ * Expects the current {@link ExpectFailure} failure message to match the provided regex, using
+ * {@code Pattern.DOTALL} to match newlines.
+ */
+ protected final void expectFailureMatches(String regex) {
+ expectThatFailure().hasMessageThat().matches(Pattern.compile(regex, Pattern.DOTALL));
+ }
+
+ /**
+ * Expects the current {@link ExpectFailure} failure message to NOT match the provided regex,
+ * using {@code Pattern.DOTALL} to match newlines.
+ */
+ protected final void expectNoRegex(Throwable t, String regex) {
+ expectThatFailure().hasMessageThat().doesNotMatch(Pattern.compile(regex, Pattern.DOTALL));
+ }
+
+ protected static final <T> ImmutableList<T> listOf(T... elements) {
+ return ImmutableList.copyOf(elements);
+ }
+
+ protected static final <T> T[] arrayOf(T... elements) {
+ return elements;
+ }
+
+ @SuppressWarnings("unchecked")
+ protected static final <K, V> ImmutableMap<K, V> mapOf(K k0, V v0, Object... rest) {
+ Preconditions.checkArgument(rest.length % 2 == 0, "Uneven args: %s", rest.length);
+
+ ImmutableMap.Builder<K, V> builder = new ImmutableMap.Builder<>();
+ builder.put(k0, v0);
+ for (int i = 0; i < rest.length; i += 2) {
+ builder.put((K) rest[i], (V) rest[i + 1]);
+ }
+ return builder.build();
+ }
+
+ @SuppressWarnings("unchecked")
+ protected static final <K, V> ImmutableMultimap<K, V> multimapOf(K k0, V v0, Object... rest) {
+ Preconditions.checkArgument(rest.length % 2 == 0, "Uneven args: %s", rest.length);
+
+ ImmutableMultimap.Builder<K, V> builder = new ImmutableMultimap.Builder<>();
+ builder.put(k0, v0);
+ for (int i = 0; i < rest.length; i += 2) {
+ builder.put((K) rest[i], (V) rest[i + 1]);
+ }
+ return builder.build();
+ }
+
+ final void checkMethodNamesEndWithForValues(
+ Class<?> clazz, Class<? extends Subject> pseudoSuperclass) {
+ // Don't run this test twice.
+ if (!testIsRunOnce()) {
+ return;
+ }
+
+ Set<String> diff = Sets.difference(getMethodNames(clazz), getMethodNames(pseudoSuperclass));
+ assertWithMessage("Found no methods to test. Bug in test?").that(diff).isNotEmpty();
+ for (String methodName : diff) {
+ assertThat(methodName).endsWith("ForValues");
+ }
+ }
+
+ private static ImmutableSet<String> getMethodNames(Class<?> clazz) {
+ ImmutableSet.Builder<String> names = ImmutableSet.builder();
+ for (Method method : clazz.getMethods()) {
+ names.add(method.getName());
+ }
+ return names.build();
+ }
+}
diff --git a/extensions/proto/src/test/proto/test_message2.proto b/extensions/proto/src/test/proto/test_message2.proto
new file mode 100644
index 00000000..547caed0
--- /dev/null
+++ b/extensions/proto/src/test/proto/test_message2.proto
@@ -0,0 +1,69 @@
+syntax = "proto2";
+
+package com.google.common.truth.extensions.proto;
+
+import "google/protobuf/any.proto";
+
+option java_package = "com.google.common.truth.extensions.proto";
+option java_multiple_files = true;
+
+// For brevity: o_ means 'optional', r_ means 'repeated'
+
+// This file must be kept in sync with test_message3.proto for tests to work.
+// Field names and field numbers should all match. Features supported in one
+// syntax but not the other are commented out in the other file.
+
+message TestMessage2 {
+ enum TestEnum2 {
+ DEFAULT = 0;
+ ONE = 1;
+ TWO = 2;
+ }
+
+ optional int32 o_int = 1;
+ repeated string r_string = 2;
+ optional int64 o_long_defaults_to_42 = 3 [default = 42];
+ optional TestEnum2 o_enum = 4;
+ optional float o_float = 5;
+ optional float o_float2 = 6;
+ optional double o_double = 7;
+ optional double o_double2 = 8;
+ optional float o_float_defaults_to_42 = 9 [default = 42.0];
+ optional double o_double_defaults_to_42 = 10 [default = 42.0];
+
+ optional RequiredStringMessage2 o_required_string_message = 11;
+ repeated RequiredStringMessage2 r_required_string_message = 12;
+ optional TestMessage2 o_test_message = 13;
+ repeated TestMessage2 r_test_message = 14;
+ optional SubTestMessage2 o_sub_test_message = 15;
+ repeated SubTestMessage2 r_sub_test_message = 16;
+ map<string, TestMessage2> test_message_map = 17;
+ optional .google.protobuf.Any o_any_message = 18;
+ repeated .google.protobuf.Any r_any_message = 19;
+
+ oneof oneof_field {
+ TestMessage2 oneof_message1 = 20;
+ TestMessage2 oneof_message2 = 21;
+ }
+}
+
+message RequiredStringMessage2 {
+ required string required_string = 1;
+}
+
+message SubTestMessage2 {
+ optional int32 o_int = 1;
+ repeated string r_string = 2;
+ optional float o_float = 3;
+ optional double o_double = 4;
+
+ optional TestMessage2 o_test_message = 5;
+ optional SubSubTestMessage2 o_sub_sub_test_message = 6;
+}
+
+message SubSubTestMessage2 {
+ optional int32 o_int = 1;
+ repeated string r_string = 2;
+ optional float o_float = 3;
+ optional double o_double = 4;
+}
diff --git a/extensions/proto/src/test/proto/test_message3.proto b/extensions/proto/src/test/proto/test_message3.proto
new file mode 100644
index 00000000..0a6c0d12
--- /dev/null
+++ b/extensions/proto/src/test/proto/test_message3.proto
@@ -0,0 +1,69 @@
+syntax = "proto3";
+
+package com.google.common.truth.extensions.proto;
+
+import "google/protobuf/any.proto";
+
+option java_package = "com.google.common.truth.extensions.proto";
+option java_multiple_files = true;
+
+// For brevity: o_ means 'optional', r_ means 'repeated'
+
+// This file must be kept in sync with test_message2.proto for tests to work.
+// Field names and field numbers should all match. Features supported in one
+// syntax but not the other are commented out in the other file.
+
+message TestMessage3 {
+ enum TestEnum3 {
+ DEFAULT = 0;
+ ONE = 1;
+ TWO = 2;
+ }
+
+ int32 o_int = 1;
+ repeated string r_string = 2;
+ // optional int64 o_long_defaults_to_42 = 3 [default = 42];
+ TestEnum3 o_enum = 4;
+ float o_float = 5;
+ float o_float2 = 6;
+ double o_double = 7;
+ double o_double2 = 8;
+ // optional float o_float_defaults_to_42 = 9 [default = 42.0];
+ // optional double o_double_defaults_to_42 = 10 [default = 42.0];
+
+ // optional RequiredStringMessage3 o_required_string_message = 11;
+ // repeated RequiredStringMessage3 r_required_string_message = 12;
+ TestMessage3 o_test_message = 13;
+ repeated TestMessage3 r_test_message = 14;
+ SubTestMessage3 o_sub_test_message = 15;
+ repeated SubTestMessage3 r_sub_test_message = 16;
+ map<string, TestMessage3> test_message_map = 17;
+ .google.protobuf.Any o_any_message = 18;
+ repeated .google.protobuf.Any r_any_message = 19;
+
+ oneof oneof_field {
+ TestMessage3 oneof_message1 = 20;
+ TestMessage3 oneof_message2 = 21;
+ }
+}
+
+// message RequiredStringMessage3 {
+// required string required_string = 1;
+// }
+
+message SubTestMessage3 {
+ int32 o_int = 1;
+ repeated string r_string = 2;
+ float o_float = 3;
+ double o_double = 4;
+
+ TestMessage3 o_test_message = 5;
+ SubSubTestMessage3 o_sub_sub_test_message = 6;
+}
+
+message SubSubTestMessage3 {
+ int32 o_int = 1;
+ repeated string r_string = 2;
+ float o_float = 3;
+ double o_double = 4;
+}
diff --git a/extensions/re2j/pom.xml b/extensions/re2j/pom.xml
new file mode 100644
index 00000000..d95e4cd1
--- /dev/null
+++ b/extensions/re2j/pom.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>com.google.truth.extensions</groupId>
+ <artifactId>truth-extensions-parent</artifactId>
+ <version>HEAD-SNAPSHOT</version>
+ </parent>
+ <artifactId>truth-re2j-extension</artifactId>
+ <name>Truth Extension for RE2J</name>
+ <description>
+ An extension for the Truth test assertion framework supporting RE2J patterns
+ </description>
+ <dependencies>
+ <dependency>
+ <groupId>com.google.truth</groupId>
+ <artifactId>truth</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.re2j</groupId>
+ <artifactId>re2j</artifactId>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+</project>
+
+
diff --git a/extensions/re2j/src/main/java/com/google/common/truth/extensions/re2j/Re2jSubjects.java b/extensions/re2j/src/main/java/com/google/common/truth/extensions/re2j/Re2jSubjects.java
new file mode 100644
index 00000000..67b1ed81
--- /dev/null
+++ b/extensions/re2j/src/main/java/com/google/common/truth/extensions/re2j/Re2jSubjects.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (c) 2015 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.re2j;
+
+import com.google.common.annotations.GwtIncompatible;
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+import com.google.re2j.Pattern;
+
+/**
+ * Truth subjects for re2j regular expressions.
+ *
+ * <p>Truth natively provides subjects for dealing with {@code java.util.regex} based regular
+ * expressions. This class is intended to provide {@code com.google.re2j} analogues to those
+ * methods.
+ */
+public final class Re2jSubjects {
+ /**
+ * Returns a subject factory for {@link String} subjects which you can use to assert things about
+ * {@link com.google.re2j.Pattern} regexes.
+ *
+ * <p>This subject does not replace Truth's built-in {@link com.google.common.truth.StringSubject}
+ * but instead provides only the methods needed to deal with regular expressions.
+ *
+ * @see com.google.common.truth.StringSubject
+ */
+ public static Subject.Factory<Re2jStringSubject, String> re2jString() {
+ return Re2jStringSubject.FACTORY;
+ }
+
+ /**
+ * Subject for {@link String} subjects which you can use to assert things about {@link
+ * com.google.re2j.Pattern} regexes.
+ *
+ * @see #re2jString
+ */
+ public static final class Re2jStringSubject extends Subject {
+ private static final Subject.Factory<Re2jStringSubject, String> FACTORY =
+ new Subject.Factory<Re2jStringSubject, String>() {
+ @Override
+ public Re2jStringSubject createSubject(FailureMetadata failureMetadata, String target) {
+ return new Re2jStringSubject(failureMetadata, target);
+ }
+ };
+
+ private final String actual;
+
+ private Re2jStringSubject(FailureMetadata failureMetadata, String subject) {
+ super(failureMetadata, subject);
+ this.actual = subject;
+ }
+
+ @Override
+ protected String actualCustomStringRepresentation() {
+ return quote(actual);
+ }
+
+ /** Fails if the string does not match the given regex. */
+ public void matches(String regex) {
+ if (!Pattern.matches(regex, actual)) {
+ failWithActual("expected to match ", regex);
+ }
+ }
+
+ /** Fails if the string does not match the given regex. */
+ @GwtIncompatible("com.google.re2j.Pattern")
+ public void matches(Pattern regex) {
+ if (!regex.matcher(actual).matches()) {
+ failWithActual("expected to match ", regex);
+ }
+ }
+
+ /** Fails if the string matches the given regex. */
+ public void doesNotMatch(String regex) {
+ if (Pattern.matches(regex, actual)) {
+ failWithActual("expected to fail to match", regex);
+ }
+ }
+
+ /** Fails if the string matches the given regex. */
+ @GwtIncompatible("com.google.re2j.Pattern")
+ public void doesNotMatch(Pattern regex) {
+ if (regex.matcher(actual).matches()) {
+ failWithActual("expected to fail to match", regex);
+ }
+ }
+
+ /** Fails if the string does not contain a match on the given regex. */
+ @GwtIncompatible("com.google.re2j.Pattern")
+ public void containsMatch(Pattern pattern) {
+ if (!pattern.matcher(actual).find()) {
+ failWithActual("expected to contain a match for", pattern);
+ }
+ }
+
+ /** Fails if the string does not contain a match on the given regex. */
+ public void containsMatch(String regex) {
+ if (!doContainsMatch(actual, regex)) {
+ failWithActual("expected to contain a match for", regex);
+ }
+ }
+
+ /** Fails if the string contains a match on the given regex. */
+ @GwtIncompatible("com.google.re2j.Pattern")
+ public void doesNotContainMatch(Pattern pattern) {
+ if (pattern.matcher(actual).find()) {
+ failWithActual("expected not to contain a match for", pattern);
+ }
+ }
+
+ /** Fails if the string contains a match on the given regex. */
+ public void doesNotContainMatch(String regex) {
+ if (doContainsMatch(actual, regex)) {
+ failWithActual("expected not to contain a match for", regex);
+ }
+ }
+
+ private static String quote(CharSequence toBeWrapped) {
+ return "\"" + toBeWrapped + "\"";
+ }
+
+ private static boolean doContainsMatch(String subject, String regex) {
+ return Pattern.compile(regex).matcher(subject).find();
+ }
+ }
+
+ private Re2jSubjects() {}
+}
diff --git a/extensions/re2j/src/test/java/com/google/common/truth/extensions/re2j/Re2jSubjectsTest.java b/extensions/re2j/src/test/java/com/google/common/truth/extensions/re2j/Re2jSubjectsTest.java
new file mode 100644
index 00000000..8c5949d2
--- /dev/null
+++ b/extensions/re2j/src/test/java/com/google/common/truth/extensions/re2j/Re2jSubjectsTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2015 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.extensions.re2j;
+
+import static com.google.common.truth.Truth.assertAbout;
+import static com.google.common.truth.extensions.re2j.Re2jSubjects.re2jString;
+
+import com.google.re2j.Pattern;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Re2jSubjects}. */
+@RunWith(JUnit4.class)
+public class Re2jSubjectsTest {
+ private static final String PATTERN_STR = "(?:hello )+world";
+ private static final Pattern PATTERN = Pattern.compile(PATTERN_STR);
+
+ @Test
+ public void matches_string_succeeds() {
+ assertAbout(re2jString()).that("hello world").matches(PATTERN_STR);
+ }
+
+ @Test
+ public void matches_pattern_succeeds() {
+ assertAbout(re2jString()).that("hello world").matches(PATTERN);
+ }
+
+ @Test
+ public void doesNotMatch_string_succeeds() {
+ assertAbout(re2jString()).that("world").doesNotMatch(PATTERN_STR);
+ }
+
+ @Test
+ public void doesNotMatch_pattern_succeeds() {
+ assertAbout(re2jString()).that("world").doesNotMatch(PATTERN);
+ }
+
+ @Test
+ public void containsMatch_string_succeeds() {
+ assertAbout(re2jString()).that("this is a hello world").containsMatch(PATTERN_STR);
+ }
+
+ @Test
+ public void containsMatch_pattern_succeeds() {
+ assertAbout(re2jString()).that("this is a hello world").containsMatch(PATTERN);
+ }
+
+ @Test
+ public void doesNotContainMatch_string_succeeds() {
+ assertAbout(re2jString()).that("hello cruel world").doesNotContainMatch(PATTERN_STR);
+ }
+
+ @Test
+ public void doesNotContainMatch_pattern_succeeds() {
+ assertAbout(re2jString()).that("hello cruel world").doesNotContainMatch(PATTERN);
+ }
+}
diff --git a/overview.html b/overview.html
new file mode 100644
index 00000000..5d2a75cd
--- /dev/null
+++ b/overview.html
@@ -0,0 +1,14 @@
+<body>
+<a href="https://truth.dev" target="_top">Truth</a> is a library for performing assertions in
+tests:
+
+<pre>{@code
+assertThat(notificationText).contains("testuser@google.com");
+}</pre>
+
+<p>Truth is owned and maintained by the <a href="http://github.com/google/guava"
+target="_top">Guava</a> team. It is used in the majority of the tests in Google’s own codebase.
+
+<p>For more information, see <a href="https://truth.dev" target="_top">our introduction</a> and
+other docs.
+</body>
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 00000000..a105364c
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,412 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.sonatype.oss</groupId>
+ <artifactId>oss-parent</artifactId>
+ <version>7</version>
+ </parent>
+ <groupId>com.google.truth</groupId>
+ <artifactId>truth-parent</artifactId>
+ <version>HEAD-SNAPSHOT</version>
+ <packaging>pom</packaging>
+ <name>Truth (Parent)</name>
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+
+ <!-- Properties for plugins for which pluginManagement hasn't been working for us. -->
+ <maven-javadoc-plugin.version>3.2.0</maven-javadoc-plugin.version>
+ <maven-enforcer-plugin.version>3.0.0-M3</maven-enforcer-plugin.version>
+
+ <!-- Properties for multiple-artifact deps. -->
+ <auto-value.version>1.8.1</auto-value.version>
+ <!--
+ We have a separate property for each flavor of Guava (instead of a shared
+ version without the -android and -jre suffixes) because that lets
+ Dependabot update our Guava versions.
+ -->
+ <guava.android.version>30.1.1-android</guava.android.version>
+ <!--
+ Also, we have this comment in between the 2 flavors of Guava. That's
+ also to smooth the Dependabot update process: Dependabot generates a
+ separate PR for each flavor. Even if we approve both at the same
+ time, one gets submitted before the other, and
+ the other ends up with a merge conflict. That requires reapprovals.
+ -->
+ <guava.jre.version>30.1.1-jre</guava.jre.version>
+ <gwt.version>2.9.0</gwt.version>
+ <protobuf.version>3.15.8</protobuf.version>
+ <!-- Property for protobuf-lite protocArtifact, which isn't a "normal" Maven dep. -->
+ <!-- TODO(cpovirk): Use protobuf.version instead. But that requires finding the new way to request the Lite runtime. -->
+ <protobuf-lite.protoc.version>3.1.0</protobuf-lite.protoc.version>
+
+ <!-- Property for an extension, since Maven doesn't have extensionManagement. -->
+ <os-maven-plugin.version>1.7.0</os-maven-plugin.version>
+ </properties>
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>com.google.truth</groupId>
+ <artifactId>truth</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.truth.extensions</groupId>
+ <artifactId>truth-liteproto-extension</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <!--
+ We could add the other modules of Truth, but there's no need because no
+ modules depend on them yet.
+ -->
+ <dependency>
+ <!--
+ In addition to setting the version of Guava that's used when Truth
+ depends directly on com.google.common:guava, this section also
+ overrides the version that's pulled in transitively by guava-gwt
+ (which is a test-scope dependency of core Truth). The Guava APIs
+ "missing" in guava-android might cause us problems down the line if we
+ actually started to run nontrivial GWT tests; I'm not sure.
+ -->
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <version>${guava.android.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.checkerframework</groupId>
+ <artifactId>checker-qual</artifactId>
+ <version>3.13.0</version>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>4.13.2</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.gwt</groupId>
+ <artifactId>gwt-user</artifactId>
+ <version>${gwt.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.auto.value</groupId>
+ <artifactId>auto-value-annotations</artifactId>
+ <version>${auto-value.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava-gwt</artifactId>
+ <version>${guava.jre.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava-testlib</artifactId>
+ <version>${guava.android.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.testing.compile</groupId>
+ <artifactId>compile-testing</artifactId>
+ <version>0.19</version>
+ <exclusions>
+ <exclusion>
+ <groupId>com.google.truth</groupId>
+ <artifactId>truth</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>com.google.errorprone</groupId>
+ <artifactId>error_prone_annotations</artifactId>
+ <version>2.6.0</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.protobuf</groupId>
+ <artifactId>protobuf-java</artifactId>
+ <version>${protobuf.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.protobuf</groupId>
+ <artifactId>protobuf-lite</artifactId>
+ <version>3.0.1</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.re2j</groupId>
+ <artifactId>re2j</artifactId>
+ <version>1.6</version>
+ </dependency>
+ <dependency>
+ <groupId>org.ow2.asm</groupId>
+ <artifactId>asm</artifactId>
+ <version>9.1</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.jsinterop</groupId>
+ <artifactId>jsinterop-annotations</artifactId>
+ <version>2.0.0</version>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+ <description>
+ Parent metadata for Truth, a Java assertion framework.
+ </description>
+ <modules>
+ <module>core</module>
+ <module>extensions</module>
+ </modules>
+ <prerequisites><maven>3.1.1</maven></prerequisites>
+ <developers>
+ <developer>
+ <id>cgruber</id>
+ <name>Christian Gruber</name>
+ <email>christianedwardgruber@gmail.com</email>
+ <url>http://www.geekinasuit.com/</url>
+ <roles>
+ <role>creator</role>
+ <role>developer</role>
+ </roles>
+ <timezone>-8</timezone>
+ </developer>
+ <developer>
+ <id>kak</id>
+ <name>Kurt Alfred Kluever</name>
+ <roles>
+ <role>evolver</role>
+ <role>developer</role>
+ </roles>
+ <timezone>-5</timezone>
+ </developer>
+ <developer>
+ <id>dsaff</id>
+ <name>David Saff</name>
+ <roles>
+ <role>creator</role>
+ <role>developer</role>
+ </roles>
+ <timezone>-5</timezone>
+ </developer>
+ <developer>
+ <id>hagbard</id>
+ <name>David B</name>
+ <roles>
+ <role>creator</role>
+ <role>developer</role>
+ </roles>
+ <timezone>+2</timezone>
+ </developer>
+ </developers>
+ <url>http://github.com/google/truth</url>
+ <licenses>
+ <license>
+ <name>The Apache Software License, Version 2.0</name>
+ <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+ <distribution>repo</distribution>
+ </license>
+ </licenses>
+ <issueManagement>
+ <system>Github</system>
+ <url>https://github.com/google/truth/issues</url>
+ </issueManagement>
+ <ciManagement>
+ <system>Jenkins</system>
+ <url>https://travis-ci.org/google/truth</url>
+ </ciManagement>
+ <scm>
+ <connection>scm:git:git@github.com:google/truth.git</connection>
+ <url>scm:git:git@github.com:google/truth.git</url>
+ </scm>
+ <build>
+ <pluginManagement>
+ <plugins>
+ <!-- https://stackoverflow.com/a/51093732/28465 -->
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-site-plugin</artifactId>
+ <version>3.9.1</version>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-project-info-reports-plugin</artifactId>
+ <version>3.1.2</version>
+ </plugin>
+ <plugin>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ <version>${maven-javadoc-plugin.version}</version>
+ <configuration>
+ <additionalOptions>
+ <additionalOption>--frames</additionalOption>
+ <additionalOption>-Xdoclint:-html</additionalOption>
+ </additionalOptions>
+ <additionalJOptions>
+ <additionalJOption>--no-module-directories</additionalJOption>
+ </additionalJOptions>
+ <doctitle>Truth ${project.version}</doctitle>
+ <windowtitle>Truth ${project.version}</windowtitle>
+ <quiet>true</quiet>
+ <notimestamp>true</notimestamp>
+ <encoding>UTF-8</encoding>
+ <docencoding>UTF-8</docencoding>
+ <charset>UTF-8</charset>
+ <overview>overview.html</overview>
+ <detectJavaApiLink>false</detectJavaApiLink>
+ <links>
+ <!-- TODO(cpovirk): Link to the version that we depend on? -->
+ <link>https://guava.dev/releases/snapshot-jre/api/docs</link>
+ <link>https://developers.google.com/protocol-buffers/docs/reference/java</link>
+ <link>https://junit.org/junit4/javadoc/latest/</link>
+ <link>https://docs.oracle.com/javase/7/docs/api/</link>
+ </links>
+ <source>8</source>
+ <sourceFileExcludes>
+ <sourceFileExclude>**/super/**/*.java</sourceFileExclude>
+ </sourceFileExcludes>
+ </configuration>
+ </plugin>
+ <plugin>
+ <artifactId>maven-jar-plugin</artifactId>
+ <version>3.2.0</version> <!-- work around ubuntu bug -->
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>animal-sniffer-maven-plugin</artifactId>
+ <version>1.20</version>
+ <configuration>
+ <signature>
+ <groupId>org.codehaus.mojo.signature</groupId>
+ <artifactId>java16-sun</artifactId>
+ <version>1.10</version>
+ </signature>
+ </configuration>
+ <executions>
+ <execution>
+ <id>check-java-version-compatibility</id>
+ <phase>test</phase>
+ <goals>
+ <goal>check</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>3.8.1</version>
+ <configuration>
+ <source>1.7</source>
+ <target>1.7</target>
+ <testSource>1.8</testSource>
+ <testTarget>1.8</testTarget>
+ </configuration>
+ </plugin>
+ <plugin>
+ <artifactId>maven-source-plugin</artifactId>
+ <version>3.2.1</version>
+ </plugin>
+ <plugin>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <version>2.22.2</version>
+ </plugin>
+ <plugin>
+ <artifactId>maven-enforcer-plugin</artifactId>
+ <version>${maven-enforcer-plugin.version}</version>
+ <executions>
+ <execution>
+ <id>enforce</id>
+ <configuration>
+ <rules>
+ <!--
+ Perhaps surprisingly, requireUpperBoundDeps catches problems
+ that dependencyConvergence does not: If we use
+ dependencyManagement to force Maven to use an *old* version
+ of a dependency, that will satisfy dependencyConvergence
+ (because the version is now consistent), but it will not
+ satisfy requireUpperBoundDeps, which apparently still sees
+ the original request for the newer version.
+ requireUpperBoundDeps's behavior is probably a good thing.
+
+ But, in what seems like a bug, dependencyConvergence catches
+ certain upper-bound problems that requireUpperBoundDeps does
+ not. To be clear, it's usually *not* a bug for
+ dependencyConvergence to give an error when
+ requireUpperBoundDeps does not: dependencyConvergence is in
+ some ways a stricter test than requireUpperBoundDeps. What
+ I'm seeing here is weirder: When I changed liteproto to
+ request checker-compat-qual 2.1.0 and
+ error_prone_annotations 2.0.9, both older versions than
+ those inherited through core Truth, dependencyConvergence
+ flagged both as expected, but requireUpperBoundDeps flagged
+ only error_prone_annotations. The reason for this may have
+ something to do with guava-25.1-android's dependency on the
+ even older checker-compat-qual 2.0.0: When I updated Truth
+ to depend on guava-26.0, which depends on
+ checker-compat-qual 2.5.3, then requireUpperBoundDeps
+ detected the problem.
+
+ I filed a bug against Maven:
+ https://issues.apache.org/jira/browse/MENFORCER-316
+ -->
+ <requireUpperBoundDeps>
+ <excludes>
+ <!-- We have some deps on guava-android and others on guava-jre. -->
+ <exclude>com.google.guava:guava</exclude>
+ </excludes>
+ </requireUpperBoundDeps>
+ <!--
+ This should be a no-op for us, since we try to list
+ everything in dependencyManagement. But it should at least
+ make sure that we do remember to put new deps into
+ dependencyManagement. It might also flag conflicts that
+ exist only in transitive dependencies. If that becomes too
+ much of a pain, we can back this check out.
+ -->
+ <dependencyConvergence />
+ <!--
+ Note that neither of these rules would catch a conflict
+ between, say, java8 and liteproto, since no Truth module
+ depends on both of those. If we wanted, we could create
+ such a module.
+ -->
+ </rules>
+ </configuration>
+ <goals>
+ <goal>enforce</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>gwt-maven-plugin</artifactId>
+ <version>${gwt.version}</version>
+ </plugin>
+ <plugin>
+ <groupId>org.xolstice.maven.plugins</groupId>
+ <artifactId>protobuf-maven-plugin</artifactId>
+ <version>0.6.1</version>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ <plugins>
+ <!--
+ Force a version >2.7 for this parent project. If we use the current
+ default of 2.7, Maven ignores this parent project's configuration when
+ running maven-javadoc-plugin in children during releases.
+ -->
+ <plugin>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ <version>${maven-javadoc-plugin.version}</version>
+ </plugin>
+ <!--
+ Similar. Without this, Maven tries to run maven-enforcer-plugin 1.0,
+ and it fails to construct an instance of the rule class, apparently
+ because of a mismatch between the new Maven APIs and the old Enforcer
+ APIs.
+ -->
+ <plugin>
+ <artifactId>maven-enforcer-plugin</artifactId>
+ <version>${maven-enforcer-plugin.version}</version>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/refactorings/src/main/java/com/google/common/truth/refactorings/CorrespondenceSubclassToFactoryCall.java b/refactorings/src/main/java/com/google/common/truth/refactorings/CorrespondenceSubclassToFactoryCall.java
new file mode 100644
index 00000000..76a85502
--- /dev/null
+++ b/refactorings/src/main/java/com/google/common/truth/refactorings/CorrespondenceSubclassToFactoryCall.java
@@ -0,0 +1,658 @@
+/*
+ * Copyright (c) 2019 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.refactorings;
+
+import static com.google.common.base.CaseFormat.UPPER_UNDERSCORE;
+import static com.google.common.base.Objects.equal;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap;
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.common.collect.MoreCollectors.onlyElement;
+import static com.google.common.collect.Streams.stream;
+import static com.google.common.truth.refactorings.CorrespondenceSubclassToFactoryCall.MemberType.COMPARE_METHOD;
+import static com.google.common.truth.refactorings.CorrespondenceSubclassToFactoryCall.MemberType.CONSTRUCTOR;
+import static com.google.common.truth.refactorings.CorrespondenceSubclassToFactoryCall.MemberType.TO_STRING_METHOD;
+import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
+import static com.google.errorprone.fixes.SuggestedFixes.compilesWithFix;
+import static com.google.errorprone.matchers.Description.NO_MATCH;
+import static com.google.errorprone.util.ASTHelpers.getDeclaredSymbol;
+import static com.google.errorprone.util.ASTHelpers.getSymbol;
+import static com.sun.source.tree.Tree.Kind.EXPRESSION_STATEMENT;
+import static com.sun.source.tree.Tree.Kind.IDENTIFIER;
+import static com.sun.source.tree.Tree.Kind.MEMBER_SELECT;
+import static com.sun.source.tree.Tree.Kind.METHOD_INVOCATION;
+import static com.sun.source.tree.Tree.Kind.NEW_CLASS;
+import static com.sun.source.tree.Tree.Kind.NULL_LITERAL;
+import static com.sun.source.tree.Tree.Kind.RETURN;
+import static java.lang.String.format;
+import static javax.lang.model.element.ElementKind.METHOD;
+import static javax.lang.model.element.Modifier.PRIVATE;
+import static javax.lang.model.element.Modifier.PROTECTED;
+import static javax.lang.model.element.Modifier.PUBLIC;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.CaseFormat;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.SetMultimap;
+import com.google.errorprone.BugPattern;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.bugpatterns.BugChecker;
+import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
+import com.google.errorprone.fixes.SuggestedFix;
+import com.google.errorprone.matchers.Description;
+import com.sun.source.tree.ClassTree;
+import com.sun.source.tree.CompilationUnitTree;
+import com.sun.source.tree.ExpressionStatementTree;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.IdentifierTree;
+import com.sun.source.tree.MemberSelectTree;
+import com.sun.source.tree.MethodInvocationTree;
+import com.sun.source.tree.MethodTree;
+import com.sun.source.tree.NewClassTree;
+import com.sun.source.tree.ReturnTree;
+import com.sun.source.tree.StatementTree;
+import com.sun.source.tree.Tree;
+import com.sun.source.util.TreeScanner;
+import com.sun.tools.javac.code.Symbol;
+import com.sun.tools.javac.code.Symbol.MethodSymbol;
+import com.sun.tools.javac.code.Symbol.TypeSymbol;
+import com.sun.tools.javac.code.Type;
+import com.sun.tools.javac.tree.JCTree;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import javax.annotation.Nullable;
+import javax.lang.model.element.Modifier;
+
+/**
+ * Refactors some subclasses of {@code Correspondence} to instead call {@code Correspondence.from}.
+ * The exact change generated for a given correspondence depends on the details of how it is defined
+ * and used.
+ */
+@BugPattern(
+ name = "CorrespondenceSubclassToFactoryCall",
+ summary = "Use the factory methods on Correspondence instead of defining a subclass.",
+ severity = SUGGESTION)
+public final class CorrespondenceSubclassToFactoryCall extends BugChecker
+ implements ClassTreeMatcher {
+ @Override
+ public Description matchClass(ClassTree tree, VisitorState state) {
+ if (!isCorrespondence(tree.getExtendsClause(), state)) {
+ return NO_MATCH;
+ }
+
+ List<CorrespondenceCode> replacements = computePossibleReplacements(tree, state);
+
+ Tree parent = state.getPath().getParentPath().getLeaf();
+ if (parent.getKind() == NEW_CLASS) {
+ // Anonymous class. Replace the whole `new Correspondence` with a Correspondence.from call.
+ for (CorrespondenceCode replacement : replacements) {
+ SuggestedFix.Builder fix = SuggestedFix.builder();
+
+ fix.replace(parent, replacement.callSite());
+
+ Tree methodOrField = findChildOfStrictlyEnclosing(state, ClassTree.class);
+ /*
+ * If a class declares multiple anonymous Correspondences, we might end up creating multiple
+ * compare() methods in the same scope. We might get away with it, depending on the types
+ * involved, or we might need to manually rename some.
+ */
+ fix.postfixWith(methodOrField, replacement.supportingMethodDefinition());
+
+ if (compilesWithFix(fix.build(), state)) {
+ return describeMatch(parent, fix.build());
+ }
+ }
+ return NO_MATCH;
+ }
+
+ Symbol classSymbol = getDeclaredSymbol(tree);
+ /*
+ * Named class. Create a Correspondence.from call, but then decide where to put it:
+ *
+ * The "safest" thing to do is to replace the body of the class with...
+ *
+ * static final Correspondence INSTANCE = Correspondence.from(...);
+ *
+ * ...and then make all callers refer to that. (As long as the class isn't top-level, we can
+ * even delete the class entirely, putting the constant in its place.)
+ *
+ * The other option is to inline that into all use sites. That's a great option if there's a
+ * single use site in a constant or helper method, in which case we produce code like...
+ *
+ * private static Correspondence makeCorrespondence() {
+ * return Correspondence.from(...);
+ * }
+ *
+ * But the danger of inlining is twofold:
+ *
+ * 1. If there are multiple callers, we'd duplicate the definition of the Correspondence.
+ *
+ * 2. Even if there's only one caller, we might inline a large chunk of code into the middle of
+ * a comparingElementsUsing call.
+ *
+ * So we use the "safe" option unless (a) there's exactly one call and (b) it's not inside a
+ * comparingElementsUsing call.
+ */
+ SetMultimap<ParentType, NewClassTree> calls = findCalls(classSymbol, state);
+ /*
+ * We also sometime see users use the named type for fields and return types, like...
+ *
+ * static final MyCorrespondence INSTANCE = new MyCorrespondence();
+ *
+ * We need to replace that with the generic Correspondence type, like...
+ *
+ * static final Correspondence<String, Integer> INSTANCE = ...;
+ */
+ Set<Tree> typeReferences = findTypeReferences(classSymbol, state);
+
+ if (calls.size() == 1 && getOnlyElement(calls.keys()) == ParentType.OTHER) {
+ // Inline it.
+ Tree call = getOnlyElement(calls.values());
+ for (CorrespondenceCode replacement : replacements) {
+ SuggestedFix.Builder fix = SuggestedFix.builder();
+ replaceTypeReferences(fix, typeReferences, state.getSourceForNode(tree.getExtendsClause()));
+ fix.replace(call, replacement.callSite());
+ fix.replace(tree, replacement.supportingMethodDefinition());
+ if (compilesWithFix(fix.build(), state)) {
+ return describeMatch(tree, fix.build());
+ }
+ }
+ return NO_MATCH;
+ }
+
+ // Declare a constant, and make use sites refer to that.
+
+ /*
+ * If we can't find any callers, then they're probably in other files, so we're going to be
+ * stuck updating them manually. To make the manual updates as simple as possible, we'll declare
+ * a constant named INSTANCE inside the Correspondence subclass (though it won't be a
+ * Correspondence subclass after our changes, just a holder class).
+ */
+ if (calls.isEmpty()) {
+ for (CorrespondenceCode replacement : replacements) {
+ SuggestedFix.Builder fix = SuggestedFix.builder();
+ replaceTypeReferences(fix, typeReferences, state.getSourceForNode(tree.getExtendsClause()));
+
+ JCTree extendsClause = (JCTree) tree.getExtendsClause();
+ // Replace everything from `extends` to the end of the class, keeping only "class Foo."
+ int startOfExtends =
+ state
+ .getSourceCode()
+ .subSequence(0, extendsClause.getStartPosition())
+ .toString()
+ .lastIndexOf("extends");
+ fix.replace(
+ startOfExtends,
+ state.getEndPosition(tree),
+ format(
+ "{ %s static final %s INSTANCE = %s; %s }",
+ visibilityModifierOnConstructor(tree),
+ state.getSourceForNode(extendsClause),
+ replacement.callSite(),
+ replacement.supportingMethodDefinition()));
+
+ for (Tree call : calls.values()) {
+ fix.replace(call, tree.getSimpleName() + ".INSTANCE");
+ }
+
+ if (compilesWithFix(fix.build(), state)) {
+ return describeMatch(tree, fix.build());
+ }
+ }
+ return NO_MATCH;
+ }
+
+ /*
+ * We found callers. Let's optimistically assume that we found them all, in which case we might
+ * as well replace the whole class with a constant.
+ */
+ for (CorrespondenceCode replacement : replacements) {
+ SuggestedFix.Builder fix = SuggestedFix.builder();
+ replaceTypeReferences(fix, typeReferences, state.getSourceForNode(tree.getExtendsClause()));
+
+ String name = CaseFormat.UPPER_CAMEL.to(UPPER_UNDERSCORE, tree.getSimpleName().toString());
+
+ // TODO(cpovirk): We're unlikely to get away with declaring everything `static`.
+ fix.replace(
+ tree,
+ format(
+ "%s static final %s %s = %s; %s",
+ visibilityModifierOnConstructor(tree),
+ state.getSourceForNode(tree.getExtendsClause()),
+ name,
+ replacement.callSite(),
+ replacement.supportingMethodDefinition()));
+
+ for (Tree call : calls.values()) {
+ fix.replace(call, name);
+ }
+
+ if (compilesWithFix(fix.build(), state)) {
+ return describeMatch(tree, fix.build());
+ }
+ }
+ return NO_MATCH;
+ }
+
+ private static void replaceTypeReferences(
+ SuggestedFix.Builder fix, Set<Tree> typeReferences, String newType) {
+ for (Tree reference : typeReferences) {
+ fix.replace(reference, newType);
+ }
+ }
+
+ private String visibilityModifierOnConstructor(ClassTree tree) {
+ MethodTree constructor =
+ tree.getMembers().stream()
+ .filter(t -> t instanceof MethodTree)
+ .map(t -> (MethodTree) t)
+ .filter(t -> t.getName().contentEquals("<init>"))
+ .findAny()
+ .get();
+ // We don't include the ModifiersTree directly in case it contains any annotations.
+ return constructor.getModifiers().getFlags().stream()
+ .filter(m -> m == PUBLIC || m == PROTECTED || m == PRIVATE)
+ .findAny()
+ .map(Modifier::toString)
+ .orElse("");
+ }
+
+ /** Returns all calls to the constructor for the given {@code classSymbol}, organized by {@linkplain ParentType whether they happen inside a call to {@code comparingElementsUsing}. */
+ private static SetMultimap<ParentType, NewClassTree> findCalls(
+ Symbol classSymbol, VisitorState state) {
+ SetMultimap<ParentType, NewClassTree> calls = HashMultimap.create();
+ new TreeScanner<Void, Void>() {
+ private ParentType parentType = ParentType.OTHER;
+
+ @Override
+ public Void visitMethodInvocation(MethodInvocationTree node, Void unused) {
+ boolean isComparingElementsUsing =
+ Optional.of(node.getMethodSelect())
+ .filter(t -> t.getKind() == MEMBER_SELECT)
+ .map(t -> (MemberSelectTree) t)
+ .filter(t -> t.getIdentifier().contentEquals("comparingElementsUsing"))
+ .isPresent();
+ if (isComparingElementsUsing) {
+ ParentType oldParentType = parentType;
+ parentType = ParentType.COMPARING_ELEMENTS_USING;
+ super.visitMethodInvocation(node, unused);
+ parentType = oldParentType;
+ } else {
+ super.visitMethodInvocation(node, unused);
+ }
+ return null;
+ }
+
+ @Override
+ public Void visitNewClass(NewClassTree node, Void unused) {
+ if (getSymbol(node.getIdentifier()).equals(classSymbol)) {
+ calls.put(parentType, node);
+ }
+ return super.visitNewClass(node, unused);
+ }
+ }.scan(state.findEnclosing(CompilationUnitTree.class), null);
+ return calls;
+ }
+
+ /**
+ * Finds all references to the name of the given type, excluding (a) when the name is defined
+ * (e.g, {@code public class MyCorrespondence}) and (b) calls to its constructor (e.g., {@code new
+ * MyCorrespondence()}). The goal is to find every reference that we aren't already modifying so
+ * that we can rewrite, e.g., fields of the given type.
+ */
+ private static Set<Tree> findTypeReferences(Symbol classSymbol, VisitorState state) {
+ Set<Tree> references = new HashSet<>();
+ new TreeScanner<Void, Void>() {
+ @Override
+ public Void scan(Tree node, Void unused) {
+ if (equal(getSymbol(node), classSymbol)
+ && getDeclaredSymbol(node) == null // Don't touch the ClassTree that we're replacing.
+ ) {
+ references.add(node);
+ }
+ return super.scan(node, unused);
+ }
+
+ @Override
+ public Void visitNewClass(NewClassTree node, Void aVoid) {
+ scan(node.getEnclosingExpression(), null);
+ // Do NOT scan node.getIdentifier().
+ scan(node.getTypeArguments(), null);
+ scan(node.getArguments(), null);
+ scan(node.getClassBody(), null);
+ return null;
+ }
+ }.scan(state.findEnclosing(CompilationUnitTree.class), null);
+ return references;
+ }
+
+ /**
+ * Whether the instantiation of a correspondence happens directly inside a call to {@code
+ * comparingElementsUsing} or not. For example, {@code comparingElementsUsing(new
+ * MyCorrespondence())} and {@code comparingElementsUsing(new Correspondence() { ... })} are both
+ * considered to have type {@link #COMPARING_ELEMENTS_USING}, but {@code private static final
+ * MyCorrespondence CORRESPONDENCE = new MyCorrespondence()} does not.
+ */
+ enum ParentType {
+ COMPARING_ELEMENTS_USING,
+ OTHER,
+ }
+
+ /**
+ * If the given correspondence implementation is "simple enough," returns one or more possible
+ * replacements for its definition and instantiation sites.
+ */
+ private static ImmutableList<CorrespondenceCode> computePossibleReplacements(
+ ClassTree classTree, VisitorState state) {
+ ImmutableSetMultimap<MemberType, Tree> members =
+ classTree.getMembers().stream()
+ .collect(toImmutableSetMultimap(m -> MemberType.from(m, state), m -> m));
+ if (members.containsKey(MemberType.OTHER)
+ || members.get(CONSTRUCTOR).size() != 1
+ || members.get(COMPARE_METHOD).size() != 1
+ || members.get(TO_STRING_METHOD).size() != 1) {
+ return ImmutableList.of();
+ }
+ MethodTree constructor = (MethodTree) getOnlyElement(members.get(CONSTRUCTOR));
+ MethodTree compareMethod = (MethodTree) getOnlyElement(members.get(COMPARE_METHOD));
+ MethodTree toStringMethod = (MethodTree) getOnlyElement(members.get(TO_STRING_METHOD));
+
+ if (!constructorCallsOnlySuper(constructor)) {
+ return ImmutableList.of();
+ }
+
+ ImmutableList<BinaryPredicateCode> binaryPredicates =
+ makeBinaryPredicates(classTree, compareMethod, state);
+
+ ExpressionTree toStringReturns = returnExpression(toStringMethod);
+ if (toStringReturns == null) {
+ return ImmutableList.of();
+ }
+ /*
+ * Replace bad toString() implementations that merely `return null`, since the factories make
+ * that an immediate error.
+ */
+ String description =
+ toStringReturns.getKind() == NULL_LITERAL
+ ? "\"corresponds to\""
+ : state.getSourceForNode(toStringReturns);
+ return binaryPredicates.stream()
+ .map(p -> CorrespondenceCode.create(p, description))
+ .collect(toImmutableList());
+ }
+
+ /** Returns one or more possible replacements for the given correspondence's {@code compare} method's definition and for code to pass to {@code Correspondence.from) to construct a correspondence that uses the replacement. */
+ private static ImmutableList<BinaryPredicateCode> makeBinaryPredicates(
+ ClassTree classTree, MethodTree compareMethod, VisitorState state) {
+ Tree comparison = maybeMakeLambdaBody(compareMethod, state);
+ if (comparison == null) {
+ ClassTree enclosing = findStrictlyEnclosing(state, ClassTree.class);
+ CharSequence newCompareMethodOwner;
+ String newCompareMethodName;
+ if (enclosing == null) {
+ newCompareMethodName = "compare";
+ newCompareMethodOwner = classTree.getSimpleName();
+ } else {
+ newCompareMethodName =
+ "compare" + classTree.getSimpleName().toString().replaceFirst("Correspondence$", "");
+ newCompareMethodOwner = enclosing.getSimpleName();
+ }
+ // TODO(cpovirk): We're unlikely to get away with declaring everything `static`.
+ String supportingMethodDefinition =
+ format(
+ "private static boolean %s(%s, %s) %s",
+ newCompareMethodName,
+ state.getSourceForNode(compareMethod.getParameters().get(0)),
+ state.getSourceForNode(compareMethod.getParameters().get(1)),
+ state.getSourceForNode(compareMethod.getBody()));
+ return ImmutableList.of(
+ BinaryPredicateCode.create(
+ newCompareMethodOwner + "::" + newCompareMethodName, supportingMethodDefinition));
+ }
+ // First try without types, then try with.
+ return ImmutableList.of(
+ BinaryPredicateCode.fromParamsAndExpression(
+ compareMethod.getParameters().get(0).getName(),
+ compareMethod.getParameters().get(1).getName(),
+ state.getSourceForNode(comparison)),
+ BinaryPredicateCode.fromParamsAndExpression(
+ state.getSourceForNode(compareMethod.getParameters().get(0)),
+ state.getSourceForNode(compareMethod.getParameters().get(1)),
+ state.getSourceForNode(comparison)));
+ }
+
+ /**
+ * Converts the given method into a lambda, either expression or block, if "appropriate." For
+ * details about the various cases, see implementation comments.
+ */
+ private static Tree maybeMakeLambdaBody(MethodTree compareMethod, VisitorState state) {
+ ExpressionTree comparison = returnExpression(compareMethod);
+ if (comparison != null) {
+ // compare() is defined as simply `return something;`. Create a lambda.
+ return comparison;
+ }
+
+ /*
+ * compare() has multiple statements. Let's keep it as a method (though we might change its
+ * modifiers, name, and location) and construct the Correspondence with a method reference...
+ *
+ * ...unless it relies on parameters from the enclosing method, in which case extracting a
+ * method isn't going to work because it won't be able to access those parameters.
+ */
+ MethodTree enclosingMethod = state.findEnclosing(MethodTree.class);
+ if (enclosingMethod == null) {
+ // No enclosing method, so we're presumably not closing over anything. Safe to extract method.
+ return null;
+ }
+
+ ImmutableSet<Symbol> paramsOfEnclosingMethod =
+ enclosingMethod.getParameters().stream()
+ .map(p -> getDeclaredSymbol(p))
+ .collect(toImmutableSet());
+ boolean[] referenceFound = new boolean[1];
+ new TreeScanner<Void, Void>() {
+ @Override
+ public Void scan(Tree node, Void aVoid) {
+ if (paramsOfEnclosingMethod.contains(getSymbol(node))) {
+ referenceFound[0] = true;
+ }
+ return super.scan(node, aVoid);
+ }
+ }.scan(state.getPath().getLeaf(), null);
+
+ if (!referenceFound[0]) {
+ // No references to anything from the enclosing method. Probably safe to extract a method.
+ return null;
+ }
+
+ /*
+ * compare() both:
+ *
+ * - uses a parameter from the enclosing method and
+ *
+ * - has multiple statements
+ *
+ * So we create a block lambda.
+ */
+ return compareMethod.getBody();
+ }
+
+ /** Like {@link VisitorState#findEnclosing} but doesn't consider the leaf to enclose itself. */
+ private static <T extends Tree> T findStrictlyEnclosing(VisitorState state, Class<T> clazz) {
+ return stream(state.getPath().getParentPath())
+ .filter(clazz::isInstance)
+ .map(clazz::cast)
+ .findAny()
+ .orElse(null);
+ }
+
+ /**
+ * Like {@link #findStrictlyEnclosing} but returns not the found element but its child along the
+ * path. For example, if called with {@code ClassTree}, it might return a {@code MethodTree}
+ * inside the class.
+ */
+ private static Tree findChildOfStrictlyEnclosing(
+ VisitorState state, Class<? extends Tree> clazz) {
+ Tree previous = state.getPath().getLeaf();
+ for (Tree t : state.getPath().getParentPath()) {
+ if (clazz.isInstance(t)) {
+ return previous;
+ }
+ previous = t;
+ }
+ return null;
+ }
+
+ /**
+ * A {@code Correspondence.from} call to replace the instantiation site of a {@code
+ * Correspondence}. Often the call is self-contained (if it's a lambda), but sometimes it's a
+ * method reference, in which case it's accompanied by a separate method definition.
+ */
+ @AutoValue
+ abstract static class CorrespondenceCode {
+ static CorrespondenceCode create(BinaryPredicateCode binaryPredicate, String description) {
+ return new AutoValue_CorrespondenceSubclassToFactoryCall_CorrespondenceCode(
+ binaryPredicate, description);
+ }
+
+ abstract BinaryPredicateCode binaryPredicate();
+
+ abstract String description();
+
+ final String callSite() {
+ return format("Correspondence.from(%s, %s)", binaryPredicate().usage(), description());
+ }
+
+ final String supportingMethodDefinition() {
+ return binaryPredicate().supportingMethodDefinition().orElse("");
+ }
+ }
+
+ /**
+ * Code that can be inserted as the first argument of a {@code Correspondence.from} call. Often
+ * it's a lambda, but sometimes it's a method reference, in which case it's accompanied by a
+ * separate method definition.
+ */
+ @AutoValue
+ abstract static class BinaryPredicateCode {
+ static BinaryPredicateCode fromParamsAndExpression(
+ CharSequence param0, CharSequence param1, String expression) {
+ return create(String.format("(%s, %s) -> %s", param0, param1, expression), null);
+ }
+
+ static BinaryPredicateCode create(String usage, @Nullable String supportingMethodDefinition) {
+ return new AutoValue_CorrespondenceSubclassToFactoryCall_BinaryPredicateCode(
+ usage, Optional.ofNullable(supportingMethodDefinition));
+ }
+
+ abstract String usage();
+
+ abstract Optional<String> supportingMethodDefinition();
+ }
+
+ private static ExpressionTree returnExpression(MethodTree method) {
+ List<? extends StatementTree> statements = method.getBody().getStatements();
+ if (statements.size() != 1) {
+ return null;
+ }
+ StatementTree statement = getOnlyElement(statements);
+ if (statement.getKind() != RETURN) {
+ return null;
+ }
+ return ((ReturnTree) statement).getExpression();
+ }
+
+ private static boolean constructorCallsOnlySuper(MethodTree constructor) {
+ List<? extends StatementTree> statements = constructor.getBody().getStatements();
+ if (statements.size() != 1) {
+ return false;
+ }
+ StatementTree statement = getOnlyElement(statements);
+ if (statement.getKind() != EXPRESSION_STATEMENT) {
+ return false;
+ }
+ ExpressionTree expression = ((ExpressionStatementTree) statement).getExpression();
+ if (expression.getKind() != METHOD_INVOCATION) {
+ return false;
+ }
+ ExpressionTree methodSelect = ((MethodInvocationTree) expression).getMethodSelect();
+ if (methodSelect.getKind() != IDENTIFIER
+ || !((IdentifierTree) methodSelect).getName().contentEquals("super")) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Whether a member is of one of the standard {@link Correspondence} methods that we know how to
+ * migrate and, if so, which one.
+ */
+ enum MemberType {
+ CONSTRUCTOR,
+ COMPARE_METHOD,
+ TO_STRING_METHOD,
+ OTHER,
+ ;
+
+ static MemberType from(Tree tree, VisitorState state) {
+ Symbol symbol = getDeclaredSymbol(tree);
+ if (!(symbol instanceof MethodSymbol)) {
+ return OTHER;
+ }
+ MethodSymbol methodSymbol = (MethodSymbol) symbol;
+
+ if (methodSymbol.getSimpleName().contentEquals("<init>")) {
+ return CONSTRUCTOR;
+ } else if (overrides(methodSymbol, CORRESPONDENCE_CLASS, "compare", state)) {
+ return COMPARE_METHOD;
+ } else if (overrides(methodSymbol, "java.lang.Object", "toString", state)) {
+ return TO_STRING_METHOD;
+ } else {
+ return OTHER;
+ }
+ }
+ }
+
+ private static boolean overrides(
+ MethodSymbol potentialOverrider, String clazz, String method, VisitorState state) {
+ Symbol overridable =
+ state.getTypeFromString(clazz).tsym.getEnclosedElements().stream()
+ .filter(s -> s.getKind() == METHOD)
+ .filter(m -> m.getSimpleName().contentEquals(method))
+ .collect(onlyElement());
+ return potentialOverrider.getSimpleName().contentEquals(method)
+ && potentialOverrider.overrides(
+ overridable, (TypeSymbol) overridable.owner, state.getTypes(), true);
+ }
+
+ private static boolean isCorrespondence(Tree supertypeTree, VisitorState state) {
+ Type correspondenceType = state.getTypeFromString(CORRESPONDENCE_CLASS);
+ if (correspondenceType == null) {
+ return false;
+ }
+ return supertypeTree != null
+ && state.getTypes().isSameType(getSymbol(supertypeTree).type, correspondenceType);
+ }
+
+ private static final String CORRESPONDENCE_CLASS = "com.google.common.truth.Correspondence";
+}
diff --git a/refactorings/src/main/java/com/google/common/truth/refactorings/FailWithFacts.java b/refactorings/src/main/java/com/google/common/truth/refactorings/FailWithFacts.java
new file mode 100644
index 00000000..d47b35cb
--- /dev/null
+++ b/refactorings/src/main/java/com/google/common/truth/refactorings/FailWithFacts.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (c) 2019 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.refactorings;
+
+import static com.google.common.base.CharMatcher.inRange;
+import static com.google.common.base.CharMatcher.whitespace;
+import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
+import static com.google.errorprone.matchers.Description.NO_MATCH;
+import static com.google.errorprone.matchers.Matchers.instanceMethod;
+import static com.google.errorprone.util.ASTHelpers.constValue;
+import static com.sun.source.tree.Tree.Kind.IDENTIFIER;
+import static com.sun.source.tree.Tree.Kind.MEMBER_SELECT;
+import static java.lang.String.format;
+import static java.util.stream.Collectors.joining;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Splitter;
+import com.google.errorprone.BugPattern;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.bugpatterns.BugChecker;
+import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
+import com.google.errorprone.fixes.SuggestedFix;
+import com.google.errorprone.matchers.Description;
+import com.google.errorprone.matchers.Matcher;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.MemberSelectTree;
+import com.sun.source.tree.MethodInvocationTree;
+import java.util.List;
+
+/**
+ * Migrates Truth subjects from the old {@code fail(String, Object)} to the new {@code
+ * failWithActual(String, Object)}, tweaking verbs for the new grammar. For example:
+ *
+ * <pre>{@code
+ * // Before:
+ * fail("has foo", expected);
+ *
+ * // After:
+ * failWithActual("expected to have foo", expected);
+ * }</pre>
+ */
+@BugPattern(
+ name = "FailWithFacts",
+ summary = "Use the new key-value-style failure API instead of the deprecated one.",
+ severity = SUGGESTION)
+public final class FailWithFacts extends BugChecker implements MethodInvocationTreeMatcher {
+ @Override
+ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
+ if (!ONE_ARG_FAIL.matches(tree, state) && !TWO_ARG_FAIL.matches(tree, state)) {
+ return NO_MATCH;
+ }
+
+ SuggestedFix.Builder fix = SuggestedFix.builder();
+ if (tree.getMethodSelect().getKind() == IDENTIFIER) {
+ fix.replace(tree.getMethodSelect(), "failWithActual");
+ } else if (tree.getMethodSelect().getKind() == MEMBER_SELECT) {
+ MemberSelectTree methodSelect = (MemberSelectTree) tree.getMethodSelect();
+ fix.replace(
+ state.getEndPosition(methodSelect.getExpression()),
+ state.getEndPosition(methodSelect),
+ ".failWithActual");
+ } else {
+ return NO_MATCH;
+ }
+
+ ExpressionTree oldVerbArg = tree.getArguments().get(0);
+ String oldVerb = constValue(oldVerbArg, String.class);
+ if (oldVerb == null) {
+ return NO_MATCH;
+ }
+ String newVerb = newVerb(oldVerb);
+ if (newVerb == null) {
+ return NO_MATCH;
+ }
+ String newVerbQuoted = state.getElements().getConstantExpression(newVerb);
+ if (ONE_ARG_FAIL.matches(tree, state)) {
+ fix.addStaticImport("com.google.common.truth.Fact.simpleFact");
+ fix.replace(oldVerbArg, format("simpleFact(%s)", newVerbQuoted));
+ } else {
+ fix.replace(oldVerbArg, newVerbQuoted);
+ }
+ return describeMatch(tree, fix.build());
+ }
+
+ private static String newVerb(String oldVerb) {
+ List<String> old = Splitter.on(whitespace()).splitToList(oldVerb);
+ String first = old.get(0);
+ if (CAPITAL_LETTER.matchesAnyOf(first)) {
+ // "hasFoo," etc. TODO(cpovirk): Handle these.
+ return null;
+ }
+ if (first.equals("does") && old.size() >= 2 && old.get(1).equals("not")) {
+ // "Not true that foo does not exist" -> "expected not to exist"
+ return "expected not to " + skip(old, 2);
+ }
+ if (first.equals("is") && old.size() >= 2 && old.get(1).equals("not")) {
+ // "Not true that foo is not visible" -> "expected not to be visible"
+ return "expected not to be " + skip(old, 2);
+ }
+ if (first.equals("has")) {
+ // "Not true that foo has children" -> "expected to have children"
+ return "expected to have " + skip(old, 1);
+ } else if (first.equals("is") || first.equals("are") || first.equals("was")) {
+ // "Not true that foo is empty" -> "expected to be empty"
+ // "Not true that operations are complete" -> "expected to be complete"
+ // "Not true that foo was deleted" -> "expected to be deleted"
+ return "expected to be " + skip(old, 1);
+ } else if (first.endsWith("ies")) {
+ // "Not true that foo applies to bar" -> "expected apply to bar"
+ return "expected to " + first.replaceFirst("ies$", "y") + " " + skip(old, 1);
+ } else if (first.endsWith("ches")) {
+ // "Not true that foo matches bar" -> "expected to match bar"
+ return "expected to " + first.replaceFirst("ches$", "ch") + " " + skip(old, 1);
+ } else if (first.matches(".*[^aeiouy]s$")) {
+ // "Not true that foo contains bar" -> "expected to contain bar"
+ return "expected to " + first.replaceFirst("s$", "") + " " + skip(old, 1);
+ } else {
+ return null;
+ }
+ }
+
+ private static String skip(List<String> old, int i) {
+ return old.stream().skip(i).collect(joining(" "));
+ }
+
+ private static final Matcher<ExpressionTree> ONE_ARG_FAIL =
+ instanceMethod()
+ .onDescendantOf("com.google.common.truth.Subject")
+ .named("fail")
+ .withParameters("java.lang.String");
+ private static final Matcher<ExpressionTree> TWO_ARG_FAIL =
+ instanceMethod()
+ .onDescendantOf("com.google.common.truth.Subject")
+ .named("fail")
+ .withParameters("java.lang.String", "java.lang.Object");
+
+ private static final CharMatcher CAPITAL_LETTER = inRange('A', 'Z');
+}
diff --git a/refactorings/src/main/java/com/google/common/truth/refactorings/NamedToWithMessage.java b/refactorings/src/main/java/com/google/common/truth/refactorings/NamedToWithMessage.java
new file mode 100644
index 00000000..411ce6dd
--- /dev/null
+++ b/refactorings/src/main/java/com/google/common/truth/refactorings/NamedToWithMessage.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (c) 2019 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.refactorings;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
+import static com.google.errorprone.matchers.Description.NO_MATCH;
+import static com.google.errorprone.matchers.Matchers.anyOf;
+import static com.google.errorprone.matchers.Matchers.instanceMethod;
+import static com.google.errorprone.matchers.Matchers.staticMethod;
+import static com.google.errorprone.util.ASTHelpers.getReceiver;
+import static com.google.errorprone.util.ASTHelpers.getSymbol;
+import static com.google.errorprone.util.ASTHelpers.isSubtype;
+import static com.sun.source.tree.Tree.Kind.MEMBER_SELECT;
+import static com.sun.source.tree.Tree.Kind.METHOD_INVOCATION;
+import static java.lang.String.format;
+import static java.util.stream.Stream.concat;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.BugPattern;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.bugpatterns.BugChecker;
+import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
+import com.google.errorprone.fixes.SuggestedFix;
+import com.google.errorprone.matchers.Description;
+import com.google.errorprone.matchers.Matcher;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.MemberSelectTree;
+import com.sun.source.tree.MethodInvocationTree;
+import com.sun.tools.javac.code.Symbol.MethodSymbol;
+import javax.annotation.Nullable;
+
+/**
+ * Migrates assertions from {@code assertThat(...).named(...)} to {@code
+ * assertWithMessage(...).that(...)} (sometimes with an {@code about} call in there, and sometimes
+ * with {@code withMessage} if using a {@code FailureStrategy} other than assert).
+ *
+ * <p>Alternatively, if setting up the infrastructure to run this migration tool is too costly,
+ * consider running a Perl-compatible regex search-and-replace like: {@code
+ * s/assertThat[(](.*)[)]\s*[.]named[(]((?:[^"\n)]|"(?:[^\\\n]|\\.)*")*)[)]/assertWithMessage($2).that($1)/g}.
+ * Such a search will not handle as many cases as this tool, and it is more likely to produce code
+ * that does not compile (or that no longer uses custom {@code Subject} classes), but it will handle
+ * many simple cases.
+ */
+@BugPattern(
+ name = "NamedToWithMessage",
+ summary = "Use assertWithMessage(...)/withMessage(...) instead of the deprecated named(...).",
+ severity = SUGGESTION)
+public final class NamedToWithMessage extends BugChecker implements MethodInvocationTreeMatcher {
+ @Override
+ public Description matchMethodInvocation(MethodInvocationTree namedCall, VisitorState state) {
+ if (!NAMED_METHOD.matches(namedCall, state)) {
+ return NO_MATCH;
+ }
+ MethodInvocationTree thatCall = findThatCall(namedCall, state);
+ if (thatCall == null) {
+ return NO_MATCH;
+ }
+ ExpressionTree namedReceiver = getReceiver(namedCall);
+ if (namedReceiver == null) {
+ return NO_MATCH;
+ }
+ String parensAndNamedArgs =
+ state
+ .getSourceCode()
+ .subSequence(
+ state.getEndPosition(namedCall.getMethodSelect()), state.getEndPosition(namedCall))
+ .toString();
+
+ SuggestedFix.Builder fix = SuggestedFix.builder();
+ // We want to do something like the following, but it overlaps with some other changes we make:
+ // fix.replace(namedCall, state.getSourceForNode(namedReceiver));
+ fix.replace(state.getEndPosition(namedReceiver), state.getEndPosition(namedCall), "");
+
+ if (STANDARD_ASSERT_THAT.matches(thatCall, state)) {
+ fix.addStaticImport("com.google.common.truth.Truth.assertWithMessage");
+ fix.replace(
+ thatCall.getMethodSelect(), format("assertWithMessage%s.that", parensAndNamedArgs));
+ return describeMatch(namedCall, fix.build());
+ }
+
+ if (ANY_ASSERT_THAT.matches(thatCall, state)) {
+ FactoryMethodName factory = tryFindFactory(thatCall, state);
+ if (factory == null) {
+ if (ONLY_GENERATE_REFERENCES_TO_FACTORIES_THAT_ALREADY_EXIST) {
+ return NO_MATCH;
+ }
+
+ // Guess at a good name for a factory, and rely on the user to create the factory later.
+ MethodSymbol assertThatSymbol = getSymbol(thatCall);
+ if (assertThatSymbol == null) {
+ return NO_MATCH;
+ }
+ String factoryMethodEnclosingClass = assertThatSymbol.owner.getQualifiedName().toString();
+ // FooSubject -> Foos:
+ String factoryMethodName =
+ assertThatSymbol.owner.getSimpleName().toString().replaceFirst("Subject$", "s");
+ // Foos -> foos:
+ factoryMethodName =
+ factoryMethodName.substring(0, 1).toLowerCase() + factoryMethodName.substring(1);
+ factory = FactoryMethodName.create(factoryMethodEnclosingClass, factoryMethodName);
+ }
+ fix.addStaticImport("com.google.common.truth.Truth.assertWithMessage");
+ fix.addStaticImport(factory.clazz() + "." + factory.method());
+ fix.replace(
+ thatCall.getMethodSelect(),
+ format("assertWithMessage%s.about(%s()).that", parensAndNamedArgs, factory.method()));
+ return describeMatch(namedCall, fix.build());
+ }
+
+ ExpressionTree thatReceiver = getReceiver(thatCall);
+ if (thatReceiver == null) {
+ return NO_MATCH;
+ }
+
+ if (STANDARD_SUBJECT_BUILDER_THAT.matches(thatCall, state)) {
+ fix.postfixWith(thatReceiver, format(".withMessage%s", parensAndNamedArgs));
+ return describeMatch(namedCall, fix.build());
+ }
+
+ if (OTHER_SUBJECT_BUILDER_THAT.matches(thatCall, state)) {
+ if (ASSERT_ABOUT.matches(thatReceiver, state)) {
+ if (thatReceiver.getKind() != METHOD_INVOCATION) {
+ return NO_MATCH;
+ }
+ ExpressionTree assertAboutSelect = ((MethodInvocationTree) thatReceiver).getMethodSelect();
+
+ fix.addStaticImport("com.google.common.truth.Truth.assertWithMessage");
+ fix.replace(assertAboutSelect, format("assertWithMessage%s.about", parensAndNamedArgs));
+ return describeMatch(namedCall, fix.build());
+ }
+
+ if (STANDARD_SUBJECT_BUILDER_ABOUT.matches(thatReceiver, state)) {
+ ExpressionTree aboutReceiver = getReceiver(thatReceiver);
+ if (aboutReceiver == null) {
+ return NO_MATCH;
+ }
+
+ fix.postfixWith(aboutReceiver, format(".withMessage%s", parensAndNamedArgs));
+ return describeMatch(namedCall, fix.build());
+ }
+ }
+
+ return NO_MATCH;
+ }
+
+ @AutoValue
+ abstract static class FactoryMethodName {
+ static FactoryMethodName create(String clazz, String method) {
+ return new AutoValue_NamedToWithMessage_FactoryMethodName(clazz, method);
+ }
+
+ static FactoryMethodName tryCreate(MethodSymbol symbol) {
+ return symbol.params.isEmpty()
+ ? create(symbol.owner.getQualifiedName().toString(), symbol.getSimpleName().toString())
+ : null;
+ }
+
+ abstract String clazz();
+
+ abstract String method();
+ }
+
+ @Nullable
+ private static FactoryMethodName tryFindFactory(
+ MethodInvocationTree assertThatCall, VisitorState state) {
+ MethodSymbol assertThatSymbol = getSymbol(assertThatCall);
+ if (assertThatSymbol == null) {
+ return null;
+ }
+ /*
+ * First, a special case for ProtoTruth.protos(). Usually the main case below finds it OK, but
+ * sometimes it misses it, I believe because it can't decide between that and
+ * IterableOfProtosSubject.iterableOfMessages.
+ */
+ if (assertThatSymbol.owner.getQualifiedName().contentEquals(PROTO_TRUTH_CLASS)) {
+ return FactoryMethodName.create(PROTO_TRUTH_CLASS, "protos");
+ }
+ ImmutableSet<MethodSymbol> factories =
+ concat(
+ // The class that assertThat is declared in:
+ assertThatSymbol.owner.getEnclosedElements().stream(),
+ // The Subject class (possibly the same; if so, toImmutableSet() will deduplicate):
+ assertThatSymbol.getReturnType().asElement().getEnclosedElements().stream())
+ .filter(s -> s instanceof MethodSymbol)
+ .map(s -> (MethodSymbol) s)
+ .filter(
+ s ->
+ returns(s, SUBJECT_FACTORY_CLASS, state)
+ || returns(s, CUSTOM_SUBJECT_BUILDER_FACTORY_CLASS, state))
+ .collect(toImmutableSet());
+ return factories.size() == 1 ? FactoryMethodName.tryCreate(getOnlyElement(factories)) : null;
+ // TODO(cpovirk): If multiple factories exist, try filtering to visible ones only.
+ }
+
+ private static boolean returns(MethodSymbol symbol, String returnType, VisitorState state) {
+ return isSubtype(symbol.getReturnType(), state.getTypeFromString(returnType), state);
+ }
+
+ private static MethodInvocationTree findThatCall(MethodInvocationTree tree, VisitorState state) {
+ while (true) {
+ if (tree.getMethodSelect().getKind() != MEMBER_SELECT) {
+ return null;
+ }
+ MemberSelectTree methodSelect = (MemberSelectTree) tree.getMethodSelect();
+ if (methodSelect.getExpression().getKind() != METHOD_INVOCATION) {
+ return null;
+ }
+ tree = (MethodInvocationTree) methodSelect.getExpression();
+ if (ANY_ASSERT_THAT.matches(tree, state)
+ || STANDARD_SUBJECT_BUILDER_THAT.matches(tree, state)
+ || OTHER_SUBJECT_BUILDER_THAT.matches(tree, state)) {
+ return tree;
+ }
+ }
+ }
+
+ private static final String TRUTH_CLASS = "com.google.common.truth.Truth";
+ private static final String PROTO_TRUTH_CLASS =
+ "com.google.common.truth.extensions.proto.ProtoTruth";
+ private static final String SUBJECT_CLASS = "com.google.common.truth.Subject";
+ private static final String SUBJECT_FACTORY_CLASS = "com.google.common.truth.Subject.Factory";
+ private static final String CUSTOM_SUBJECT_BUILDER_FACTORY_CLASS =
+ "com.google.common.truth.CustomSubjectBuilder.Factory";
+ private static final String STANDARD_SUBJECT_BUILDER_CLASS =
+ "com.google.common.truth.StandardSubjectBuilder";
+ private static final String CUSTOM_SUBJECT_BUILDER_CLASS =
+ "com.google.common.truth.CustomSubjectBuilder";
+ private static final String SIMPLE_SUBJECT_BUILDER_CLASS =
+ "com.google.common.truth.SimpleSubjectBuilder";
+
+ private static final Matcher<ExpressionTree> STANDARD_ASSERT_THAT =
+ staticMethod().onClass(TRUTH_CLASS).named("assertThat");
+ private static final Matcher<ExpressionTree> ANY_ASSERT_THAT =
+ staticMethod().anyClass().named("assertThat");
+ private static final Matcher<ExpressionTree> ASSERT_ABOUT =
+ staticMethod().onClass(TRUTH_CLASS).named("assertAbout");
+
+ private static final Matcher<ExpressionTree> STANDARD_SUBJECT_BUILDER_THAT =
+ instanceMethod().onDescendantOf(STANDARD_SUBJECT_BUILDER_CLASS).named("that");
+ private static final Matcher<ExpressionTree> STANDARD_SUBJECT_BUILDER_ABOUT =
+ instanceMethod().onDescendantOf(STANDARD_SUBJECT_BUILDER_CLASS).named("about");
+ private static final Matcher<ExpressionTree> OTHER_SUBJECT_BUILDER_THAT =
+ anyOf(
+ instanceMethod().onDescendantOf(CUSTOM_SUBJECT_BUILDER_CLASS).named("that"),
+ instanceMethod().onDescendantOf(SIMPLE_SUBJECT_BUILDER_CLASS).named("that"));
+ private static final Matcher<ExpressionTree> NAMED_METHOD =
+ instanceMethod().onDescendantOf(SUBJECT_CLASS).named("named");
+
+ // TODO(cpovirk): Provide a flag for this.
+ private static final boolean ONLY_GENERATE_REFERENCES_TO_FACTORIES_THAT_ALREADY_EXIST = true;
+}
diff --git a/refactorings/src/main/java/com/google/common/truth/refactorings/StoreActualValueInField.java b/refactorings/src/main/java/com/google/common/truth/refactorings/StoreActualValueInField.java
new file mode 100644
index 00000000..ea2e7e21
--- /dev/null
+++ b/refactorings/src/main/java/com/google/common/truth/refactorings/StoreActualValueInField.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright (c) 2019 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.refactorings;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
+import static com.google.errorprone.fixes.SuggestedFix.replace;
+import static com.google.errorprone.matchers.Description.NO_MATCH;
+import static com.google.errorprone.matchers.Matchers.anyOf;
+import static com.google.errorprone.matchers.Matchers.constructor;
+import static com.google.errorprone.matchers.Matchers.instanceMethod;
+import static com.google.errorprone.matchers.Matchers.staticMethod;
+import static com.google.errorprone.util.ASTHelpers.getType;
+import static com.google.errorprone.util.ASTHelpers.isSameType;
+import static com.google.errorprone.util.ASTHelpers.isSubtype;
+import static com.sun.source.tree.Tree.Kind.CLASS;
+import static com.sun.source.tree.Tree.Kind.IDENTIFIER;
+import static com.sun.source.tree.Tree.Kind.MEMBER_SELECT;
+import static com.sun.source.tree.Tree.Kind.METHOD_INVOCATION;
+import static com.sun.source.tree.Tree.Kind.VARIABLE;
+import static java.lang.String.format;
+import static javax.lang.model.element.Modifier.STATIC;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.BugPattern;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.bugpatterns.BugChecker;
+import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
+import com.google.errorprone.fixes.SuggestedFix;
+import com.google.errorprone.matchers.Description;
+import com.google.errorprone.matchers.Matcher;
+import com.sun.source.tree.ClassTree;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.IdentifierTree;
+import com.sun.source.tree.MemberSelectTree;
+import com.sun.source.tree.MethodInvocationTree;
+import com.sun.source.tree.MethodTree;
+import com.sun.source.tree.Tree;
+import com.sun.source.tree.VariableTree;
+import com.sun.source.util.TreePath;
+import com.sun.tools.javac.code.Symbol;
+import com.sun.tools.javac.code.Type;
+import com.sun.tools.javac.code.Types;
+import com.sun.tools.javac.tree.JCTree;
+import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
+import com.sun.tools.javac.tree.TreeScanner;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Stream;
+import javax.lang.model.element.Name;
+
+/**
+ * Refactors callers of {@code Subject.actual()} and {@code Subject.getSubject()} to store their own
+ * copy of the actual value in a variable and use that instead.
+ */
+@BugPattern(
+ name = "StoreActualValueInField",
+ summary =
+ "Store the actual value locally instead of using the deprecated actual() and getSubject().",
+ severity = SUGGESTION)
+public final class StoreActualValueInField extends BugChecker
+ implements MethodInvocationTreeMatcher {
+ @Override
+ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
+ ClassTree enclosingClass = state.findEnclosing(ClassTree.class);
+ if (enclosingClass == null) {
+ return NO_MATCH;
+ }
+ if (enclosingClass.getMembers().stream()
+ .filter(t -> t.getKind() == VARIABLE)
+ .map(t -> (VariableTree) t)
+ .anyMatch(t -> t.getName().contentEquals("actual"))) {
+ return NO_MATCH;
+ }
+
+ if (ACTUAL_METHOD.matches(tree, state)) {
+ if (tree.getMethodSelect().getKind() == IDENTIFIER) {
+ if (varNamedActualInScope(state)) {
+ return describeMatch(tree, replace(tree, qualifierForThis(state) + "this.actual"));
+ } else {
+ return describeMatch(tree, replace(tree, "actual"));
+ }
+ } else if (tree.getMethodSelect().getKind() == MEMBER_SELECT) {
+ MemberSelectTree methodSelect = (MemberSelectTree) tree.getMethodSelect();
+ return describeMatch(
+ tree,
+ replace(
+ tree, format("%s.actual", state.getSourceForNode(methodSelect.getExpression()))));
+ } else {
+ return NO_MATCH;
+ }
+ }
+
+ if (!SUBJECT_CONSTRUCTOR_CALL.matches(tree, state)) {
+ return NO_MATCH;
+ }
+ if (tree.getMethodSelect().getKind() != IDENTIFIER
+ || !((IdentifierTree) tree.getMethodSelect()).getName().contentEquals("super")) {
+ return NO_MATCH;
+ }
+ IdentifierTree value = findActualArg(tree.getArguments(), state);
+ if (value == null) {
+ return NO_MATCH;
+ }
+ /*
+ * TODO(cpovirk): Before adding the field, scan the compilation unit for any usages of
+ * ThisType.actual(). (But this is moderately rare and usually easy to detect after the fact.
+ * Plus, adding the field in all cases is harmless enough.)
+ */
+ SuggestedFix.Builder fix = SuggestedFix.builder();
+ fix.postfixWith(
+ state.getPath().getParentPath().getLeaf(),
+ format("this.actual = %s;", state.getSourceForNode(value)));
+
+ Tree type = findActualFormalType(value.getName(), state);
+ Tree putFieldBefore =
+ enclosingClass.getMembers().stream()
+ .map((Tree t) -> t) // Stream<? extends Tree> -> Stream<Tree>
+ .filter(
+ t ->
+ t.getKind() == VARIABLE
+ && !((VariableTree) t).getModifiers().getFlags().contains(STATIC))
+ .findFirst()
+ .orElse(state.findEnclosing(MethodTree.class));
+ fix.prefixWith(
+ putFieldBefore, format("private final %s actual;", state.getSourceForNode(type)));
+
+ return describeMatch(tree, fix.build());
+ }
+
+ private static String qualifierForThis(VisitorState state) {
+ Type subjectBaseType = state.getTypeFromString("com.google.common.truth.Subject");
+
+ boolean seenClassInBetween = false;
+ for (Tree t : state.getPath()) {
+ if (t.getKind() != CLASS) {
+ continue;
+ }
+ Type enclosingType = getType(t);
+ if (isSubtype(enclosingType, subjectBaseType, state)) {
+ if (seenClassInBetween) {
+ return enclosingType.asElement().getSimpleName() + ".";
+ } else {
+ return "";
+ }
+ }
+ seenClassInBetween = true;
+ }
+ return ""; // not sure what's going on, so let's try this
+ }
+
+ // from an old copy of RenameField (Similar code now lives in FieldRenamer.)
+ private static boolean varNamedActualInScope(VisitorState state) {
+ final AtomicBoolean local = new AtomicBoolean(false);
+
+ MethodTree outerMostMethod = null;
+ for (TreePath path = state.getPath(); path != null; path = path.getParentPath()) {
+ if (path.getLeaf() instanceof MethodTree) {
+ outerMostMethod = (MethodTree) path.getLeaf();
+ }
+ }
+ if (outerMostMethod != null && outerMostMethod.getBody() != null) {
+ ((JCTree) outerMostMethod.getBody())
+ .accept(
+ new TreeScanner() {
+ @Override
+ public void visitVarDef(JCVariableDecl tree) {
+ if (tree.getName().contentEquals("actual")) {
+ local.set(true);
+ }
+ super.visitVarDef(tree);
+ }
+ });
+ }
+ return local.get();
+ }
+
+ // from AbstractCollectionIncompatibleTypeMatcher
+ private static Type extractTypeArgAsMemberOfSupertype(
+ Type type, Symbol superTypeSym, int typeArgIndex, Types types) {
+ Type collectionType = types.asSuper(type, superTypeSym);
+ if (collectionType == null) {
+ return null;
+ }
+ com.sun.tools.javac.util.List<Type> tyargs = collectionType.getTypeArguments();
+ if (tyargs.size() <= typeArgIndex) {
+ // Collection is raw, nothing we can do.
+ return null;
+ }
+
+ return tyargs.get(typeArgIndex);
+ }
+
+ private static IdentifierTree findActualArg(
+ List<? extends ExpressionTree> args, VisitorState state) {
+ Type actualType =
+ extractTypeArgAsMemberOfSupertype(
+ getType(state.findEnclosing(ClassTree.class)),
+ state.getSymbolFromString("com.google.common.truth.Subject"),
+ 1,
+ state.getTypes());
+ Type failureMetadataType = state.getTypeFromString("com.google.common.truth.FailureMetadata");
+ ImmutableSet<IdentifierTree> candidates =
+ args.stream()
+ .flatMap(a -> maybeToIdentifier(a, state))
+ .filter(a -> !isSameType(getType(a), failureMetadataType, state))
+ .filter(a -> isSubtype(getType(a), actualType, state))
+ .collect(toImmutableSet());
+ if (candidates.size() == 1) {
+ return getOnlyElement(candidates);
+ }
+
+ if (args.size() == 2
+ && isSameType(getType(args.get(0)), failureMetadataType, state)
+ && args.get(1).getKind() == IDENTIFIER) {
+ return (IdentifierTree) args.get(1);
+ }
+ return null;
+ }
+
+ private static Stream<IdentifierTree> maybeToIdentifier(ExpressionTree tree, VisitorState state) {
+ if (tree.getKind() == IDENTIFIER) {
+ return Stream.of((IdentifierTree) tree);
+ } else if (tree.getKind() == METHOD_INVOCATION && CHECK_NOT_NULL.matches(tree, state)) {
+ /*
+ * checkNotNull() is inadvisable (since it makes assertThat(foo) throw NPE for that type, even
+ * if the assertion is going to be something like isNull()). But people do it, albeit rarely.
+ */
+ MethodInvocationTree invocation = (MethodInvocationTree) tree;
+ return maybeToIdentifier(invocation.getArguments().get(0), state);
+ } else {
+ return Stream.empty();
+ }
+ }
+
+ private static Tree findActualFormalType(Name name, VisitorState state) {
+ MethodTree method = state.findEnclosing(MethodTree.class);
+ if (method == null) {
+ return null;
+ }
+ return method.getParameters().stream()
+ .filter(p -> p.getName().equals(name))
+ .findFirst()
+ .map(p -> p.getType())
+ .orElse(null);
+ }
+
+ private static final Matcher<ExpressionTree> SUBJECT_CONSTRUCTOR_CALL =
+ constructor()
+ .forClass(
+ (type, state) ->
+ isSubtype(
+ type, state.getTypeFromString("com.google.common.truth.Subject"), state));
+ private static final Matcher<ExpressionTree> ACTUAL_METHOD =
+ anyOf(
+ instanceMethod()
+ .onDescendantOf("com.google.common.truth.Subject")
+ .named("actual")
+ .withParameters(),
+ instanceMethod()
+ .onDescendantOf("com.google.common.truth.Subject")
+ .named("getSubject")
+ .withParameters());
+ private static final Matcher<ExpressionTree> CHECK_NOT_NULL =
+ staticMethod().onClass("com.google.common.base.Preconditions").named("checkNotNull");
+}
diff --git a/refactorings/src/test/java/com/google/common/truth/refactorings/CorrespondenceSubclassToFactoryCallTest.java b/refactorings/src/test/java/com/google/common/truth/refactorings/CorrespondenceSubclassToFactoryCallTest.java
new file mode 100644
index 00000000..bfef2add
--- /dev/null
+++ b/refactorings/src/test/java/com/google/common/truth/refactorings/CorrespondenceSubclassToFactoryCallTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2019 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.refactorings;
+
+import com.google.errorprone.BugCheckerRefactoringTestHelper;
+import com.google.errorprone.CompilationTestHelper;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** @author cpovirk@google.com (Chris Povirk) */
+@RunWith(JUnit4.class)
+public class CorrespondenceSubclassToFactoryCallTest {
+ private BugCheckerRefactoringTestHelper refactoringHelper;
+ private CompilationTestHelper compilationHelper;
+
+ @Before
+ public void setUp() {
+ compilationHelper =
+ CompilationTestHelper.newInstance(CorrespondenceSubclassToFactoryCall.class, getClass());
+ refactoringHelper =
+ BugCheckerRefactoringTestHelper.newInstance(
+ CorrespondenceSubclassToFactoryCall.class, getClass());
+ }
+
+ @Test
+ public void testPositiveCase() {
+ compilationHelper
+ .addSourceFile("CorrespondenceSubclassToFactoryCallPositiveCases.java")
+ .doTest();
+ }
+
+ @Test
+ public void testPositiveCase2() {
+ compilationHelper
+ .addSourceFile("CorrespondenceSubclassToFactoryCallPositiveCases2.java")
+ .doTest();
+ }
+
+ @Test
+ public void testNegativeCase() {
+ compilationHelper
+ .addSourceFile("CorrespondenceSubclassToFactoryCallNegativeCases.java")
+ .doTest();
+ }
+
+ @Test
+ public void refactoring() {
+ refactoringHelper
+ .addInput("CorrespondenceSubclassToFactoryCallPositiveCases.java")
+ .addOutput("CorrespondenceSubclassToFactoryCallPositiveCases_expected.java")
+ .doTest();
+ }
+
+ @Test
+ public void refactoring2() {
+ refactoringHelper
+ .addInput("CorrespondenceSubclassToFactoryCallPositiveCases2.java")
+ .addOutput("CorrespondenceSubclassToFactoryCallPositiveCases2_expected.java")
+ .doTest();
+ }
+}
diff --git a/refactorings/src/test/java/com/google/common/truth/refactorings/FailWithFactsTest.java b/refactorings/src/test/java/com/google/common/truth/refactorings/FailWithFactsTest.java
new file mode 100644
index 00000000..e6a53b34
--- /dev/null
+++ b/refactorings/src/test/java/com/google/common/truth/refactorings/FailWithFactsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2019 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.refactorings;
+
+import com.google.errorprone.CompilationTestHelper;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** @author cpovirk@google.com (Chris Povirk) */
+@RunWith(JUnit4.class)
+public class FailWithFactsTest {
+ private CompilationTestHelper compilationHelper;
+
+ @Before
+ public void setUp() {
+ compilationHelper = CompilationTestHelper.newInstance(FailWithFacts.class, getClass());
+ }
+
+ @Test
+ public void testPositiveCase() {
+ compilationHelper.addSourceFile("FailWithFactsPositiveCases.java").doTest();
+ }
+}
diff --git a/refactorings/src/test/java/com/google/common/truth/refactorings/NamedToWithMessageTest.java b/refactorings/src/test/java/com/google/common/truth/refactorings/NamedToWithMessageTest.java
new file mode 100644
index 00000000..e4fda26d
--- /dev/null
+++ b/refactorings/src/test/java/com/google/common/truth/refactorings/NamedToWithMessageTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2019 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.refactorings;
+
+import com.google.errorprone.CompilationTestHelper;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** @author cpovirk@google.com (Chris Povirk) */
+@RunWith(JUnit4.class)
+public class NamedToWithMessageTest {
+ private CompilationTestHelper compilationHelper;
+
+ @Before
+ public void setUp() {
+ compilationHelper = CompilationTestHelper.newInstance(NamedToWithMessage.class, getClass());
+ }
+
+ @Test
+ public void testPositiveCase() {
+ compilationHelper.addSourceFile("NamedToWithMessagePositiveCases.java").doTest();
+ }
+
+ @Test
+ public void testNegativeCase() {
+ compilationHelper.addSourceFile("NamedToWithMessageNegativeCases.java").doTest();
+ }
+}
diff --git a/refactorings/src/test/java/com/google/common/truth/refactorings/StoreActualValueInFieldTest.java b/refactorings/src/test/java/com/google/common/truth/refactorings/StoreActualValueInFieldTest.java
new file mode 100644
index 00000000..d037bf66
--- /dev/null
+++ b/refactorings/src/test/java/com/google/common/truth/refactorings/StoreActualValueInFieldTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2019 Google, Inc.
+ *
+ * Licensed 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 com.google.common.truth.refactorings;
+
+import com.google.errorprone.BugCheckerRefactoringTestHelper;
+import com.google.errorprone.CompilationTestHelper;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** @author cpovirk@google.com (Chris Povirk) */
+@RunWith(JUnit4.class)
+public class StoreActualValueInFieldTest {
+ private BugCheckerRefactoringTestHelper refactoringHelper;
+ private CompilationTestHelper compilationHelper;
+
+ @Before
+ public void setUp() {
+ compilationHelper =
+ CompilationTestHelper.newInstance(StoreActualValueInField.class, getClass());
+ refactoringHelper =
+ BugCheckerRefactoringTestHelper.newInstance(new StoreActualValueInField(), getClass());
+ }
+
+ @Test
+ public void testPositiveCase() {
+ compilationHelper.addSourceFile("StoreActualValueInFieldPositiveCases.java").doTest();
+ }
+
+ @Test
+ public void testNegativeCase() {
+ compilationHelper.addSourceFile("StoreActualValueInFieldNegativeCases.java").doTest();
+ }
+
+ @Test
+ public void refactoring() {
+ refactoringHelper
+ .addInput("StoreActualValueInFieldPositiveCases.java")
+ .addOutput("StoreActualValueInFieldPositiveCases_expected.java")
+ .doTest();
+ }
+}
diff --git a/util/generate-latest-docs.sh b/util/generate-latest-docs.sh
new file mode 100755
index 00000000..6fc96f36
--- /dev/null
+++ b/util/generate-latest-docs.sh
@@ -0,0 +1,49 @@
+#!/bin/bash
+# A script to automatically deploy javadocs.
+#
+# This script is useful both in a CI regular build, where it will generate
+# javadocs (aggregated) via a maven build, and deploy them to github pages. This script
+# is derived from instructions given in this blog article:
+# http://benlimmer.com/2013/12/26/automatically-publish-javadoc-to-gh-pages-with-travis-ci/
+#
+# If the RELEASE_VERSION environment variable is set, then it will perform a similar
+# action, but push to a versioned api docs folder, under the current user's credentials
+# rather than using an encrypted secret via github's GH_TOKEN mechanism. Users who
+# use the script this way must have a .ssh key which they have declared on github.com
+# per the instructions here: https://help.github.com/articles/generating-ssh-keys/
+#
+set -euE
+
+echo -e "Publishing javadoc...\n"
+
+if [ -n "${RELEASE_VERSION:-}" ]; then
+ # Release
+ version_subdir=api/${RELEASE_VERSION}
+ commit_message="Release $RELEASE_VERSION javadoc pushed to gh-pages."
+else
+ # CI
+ version_subdir=api/latest
+ commit_message="Latest javadoc on successful CI build auto-pushed to gh-pages."
+fi
+
+mvn javadoc:aggregate
+perl -ni -e 'print unless /Tolerant.*Comparison/ || /SubjectBuilderCallback/ || /UsingCorrespondence/ || /AsIterable/ || /Correspondence[.][A-Z]/ || /FluentAssertion/ || /PathSubject/ || /Re2jSubjects/ || /Ordered/' target/site/apidocs/allclasses-frame.html
+find target/site/apidocs -name '*.html' | xargs perl -077pi -e 's#<li class="blockList"><a name="nested.classes.inherited.from.class.com.google.common.truth.\w*Subject">.*?</li>##msg; if (m#<!-- ======== NESTED CLASS SUMMARY ======== -->(.*?)(?=<!-- =)#ms) { if ($1 !~ m#nested.classes.inherited.from|memberSummary#) { s#<!-- ======== NESTED CLASS SUMMARY ======== -->.*?(?=<!-- =)##msg; } }'
+target_dir="$(pwd)/target"
+cd ${target_dir}
+rm -rf gh-pages
+git clone --quiet --branch=gh-pages "https://x-access-token:${GITHUB_TOKEN}@github.com/google/truth.git" gh-pages > /dev/null
+cd gh-pages
+
+if [[ -z "${RELEASE_VERSION:-}" ]]; then
+ git config --global user.name "$GITHUB_ACTOR"
+ git config --global user.email "$GITHUB_ACTOR@users.noreply.github.com"
+fi
+api_version_dir="${target_dir}/gh-pages/${version_subdir}"
+git rm -rf ${api_version_dir} || true
+cp -ar ${target_dir}/site/apidocs ${api_version_dir}
+git add -A -f ${api_version_dir}
+git commit -m "${commit_message}"
+git push -fq origin gh-pages > /dev/null
+
+echo -e "Published Javadoc to gh-pages.\n"
diff --git a/util/mvn-deploy.sh b/util/mvn-deploy.sh
new file mode 100755
index 00000000..30f93cb8
--- /dev/null
+++ b/util/mvn-deploy.sh
@@ -0,0 +1,53 @@
+#!/bin/bash
+keys="$(gpg --list-keys | grep ^pub | sed 's#/# #' | awk '{ print $3 }')"
+key_count="$(echo ${keys} | wc -w)"
+
+seen=""
+while [[ $# > 0 ]] ; do
+ param="$1"
+ if [[ $param == "--signing-key" ]]; then
+ # disambiguating or overriding key
+ key="$2"
+ shift
+ else
+ seen="${seen} ${param}"
+ fi
+ shift
+done
+params=${seen}
+
+if [[ ${key_count} -lt 1 ]]; then
+ echo ""
+ echo "You are attempting to deploy a maven release without a GPG signing key."
+ echo "You need to generate a signing key in accordance with the instructions"
+ echo "found at http://blog.sonatype.com/2010/01/how-to-generate-pgp-signatures-with-maven"
+ exit 1
+fi
+
+# if a key is specified, use that, else use the default, unless there are many
+if [[ -n "${key}" ]]; then
+ #validate key
+ keystatus=$(gpg --list-keys | grep ${key} | awk '{print $1}')
+ if [ "${keystatus}" != "pub" ]; then
+ echo ""
+ echo "Could not find public key with label \"${key}\""
+ echo ""
+ echo "Available keys from: "
+ gpg --list-keys | grep --invert-match '^sub'
+ exit 1
+ fi
+
+ key_param="-Dgpg.keyname=${key}"
+elif [ ${key_count} -gt 1 ]; then
+ echo ""
+ echo "You are attempting to deploy a maven release but have more than one GPG"
+ echo "signing key and did not specify which one you wish to sign with."
+ echo ""
+ echo "usage $0 [--signing-key <ssl-key>] [<maven params> ...]"
+ echo ""
+ echo -n "Available keys from: "
+ gpg --list-keys | grep --invert-match '^sub'
+ exit 1;
+fi
+
+mvn ${params} clean site:jar -P sonatype-oss-release ${key_param} deploy