aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firestore/Example/Tests/Util
diff options
context:
space:
mode:
authorGravatar Gil <mcg@google.com>2017-10-03 08:55:22 -0700
committerGravatar GitHub <noreply@github.com>2017-10-03 08:55:22 -0700
commitbde743ed25166a0b320ae157bfb1d68064f531c9 (patch)
tree4dd7525d9df32fa5dbdb721d4b0d4f9b87f5e884 /Firestore/Example/Tests/Util
parentbf550507ffa8beee149383a5bf1e2363bccefbb4 (diff)
Release 4.3.0 (#327)
Initial release of Firestore at 0.8.0 Bump FirebaseCommunity to 0.1.3
Diffstat (limited to 'Firestore/Example/Tests/Util')
-rw-r--r--Firestore/Example/Tests/Util/FSTAssertTests.m105
-rw-r--r--Firestore/Example/Tests/Util/FSTComparisonTests.m143
-rw-r--r--Firestore/Example/Tests/Util/FSTEventAccumulator.h41
-rw-r--r--Firestore/Example/Tests/Util/FSTEventAccumulator.m94
-rw-r--r--Firestore/Example/Tests/Util/FSTHelpers.h258
-rw-r--r--Firestore/Example/Tests/Util/FSTHelpers.m348
-rw-r--r--Firestore/Example/Tests/Util/FSTIntegrationTestCase.h94
-rw-r--r--Firestore/Example/Tests/Util/FSTIntegrationTestCase.m285
-rw-r--r--Firestore/Example/Tests/Util/FSTUtilTests.m35
-rw-r--r--Firestore/Example/Tests/Util/XCTestCase+Await.h32
-rw-r--r--Firestore/Example/Tests/Util/XCTestCase+Await.m38
11 files changed, 1473 insertions, 0 deletions
diff --git a/Firestore/Example/Tests/Util/FSTAssertTests.m b/Firestore/Example/Tests/Util/FSTAssertTests.m
new file mode 100644
index 0000000..f0734df
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTAssertTests.m
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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.
+ */
+
+#import "Util/FSTAssert.h"
+
+#import <XCTest/XCTest.h>
+
+@interface FSTAssertTests : XCTestCase
+@end
+
+@implementation FSTAssertTests
+
+- (void)testFail {
+ @try {
+ [self failingMethod];
+ XCTFail("Should not have succeeded");
+ } @catch (NSException *ex) {
+ XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException);
+ XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: 0:foo:bar");
+ }
+}
+
+// A method guaranteed to fail. Note that the return type is intentionally something that is
+// not actually returned in the body to ensure that the function attribute declarations in the
+// macro properly mark this macro invocation as never returning.
+- (int)failingMethod {
+ FSTFail(@"%d:%s:%@", 0, "foo", @"bar");
+}
+
+- (void)testCFail {
+ @try {
+ failingFunction();
+ XCTFail("Should not have succeeded");
+ } @catch (NSException *ex) {
+ XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException);
+ XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: 0:foo:bar");
+ }
+}
+
+// A function guaranteed to fail. Note that the return type is intentionally something that is
+// not actually returned in the body to ensure that the function attribute declarations in the
+// macro properly mark this macro invocation as never returning.
+int failingFunction() {
+ FSTCFail(@"%d:%s:%@", 0, "foo", @"bar");
+}
+
+- (void)testAssertNonFailing {
+ @try {
+ FSTAssert(YES, @"shouldn't fail");
+ } @catch (NSException *ex) {
+ XCTFail("Should not have failed, but got %@", ex);
+ }
+}
+
+- (void)testAssertFailing {
+ @try {
+ FSTAssert(NO, @"should fail");
+ XCTFail("Should not have succeeded");
+ } @catch (NSException *ex) {
+ XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException);
+ XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: should fail");
+ }
+}
+
+- (void)testCAssertNonFailing {
+ @try {
+ nonAssertingFunction();
+ } @catch (NSException *ex) {
+ XCTFail("Should not have failed, but got %@", ex);
+ }
+}
+
+int nonAssertingFunction() {
+ FSTCAssert(YES, @"shouldn't fail");
+ return 0;
+}
+
+- (void)testCAssertFailing {
+ @try {
+ assertingFunction();
+ XCTFail("Should not have succeeded");
+ } @catch (NSException *ex) {
+ XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException);
+ XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: should fail");
+ }
+}
+
+int assertingFunction() {
+ FSTCAssert(NO, @"should fail");
+}
+
+@end
diff --git a/Firestore/Example/Tests/Util/FSTComparisonTests.m b/Firestore/Example/Tests/Util/FSTComparisonTests.m
new file mode 100644
index 0000000..9728f20
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTComparisonTests.m
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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.
+ */
+
+#import "Util/FSTComparison.h"
+
+#import <XCTest/XCTest.h>
+
+union DoubleBits {
+ double d;
+ uint64_t bits;
+};
+
+#define ASSERT_BIT_EQUALS(expected, actual) \
+ do { \
+ union DoubleBits expectedBits = {.d = expected}; \
+ union DoubleBits actualBits = {.d = expected}; \
+ if (expectedBits.bits != actualBits.bits) { \
+ XCTFail(@"Expected <%f> to compare equal to <%f> with bits <%llX> equal to <%llX>", actual, \
+ expected, actualBits.bits, expectedBits.bits); \
+ } \
+ } while (0);
+
+#define ASSERT_ORDERED_SAME(doubleValue, longValue) \
+ do { \
+ NSComparisonResult result = FSTCompareMixed(doubleValue, longValue); \
+ if (result != NSOrderedSame) { \
+ XCTFail(@"Expected <%f> to compare equal to <%lld>", doubleValue, longValue); \
+ } \
+ } while (0);
+
+#define ASSERT_ORDERED_DESCENDING(doubleValue, longValue) \
+ do { \
+ NSComparisonResult result = FSTCompareMixed(doubleValue, longValue); \
+ if (result != NSOrderedDescending) { \
+ XCTFail(@"Expected <%f> to compare equal to <%lld>", doubleValue, longValue); \
+ } \
+ } while (0);
+
+#define ASSERT_ORDERED_ASCENDING(doubleValue, longValue) \
+ do { \
+ NSComparisonResult result = FSTCompareMixed(doubleValue, longValue); \
+ if (result != NSOrderedAscending) { \
+ XCTFail(@"Expected <%f> to compare equal to <%lld>", doubleValue, longValue); \
+ } \
+ } while (0);
+
+@interface FSTComparisonTests : XCTestCase
+@end
+
+@implementation FSTComparisonTests
+
+- (void)testMixedComparison {
+ // Infinities
+ ASSERT_ORDERED_ASCENDING(-INFINITY, LLONG_MIN);
+ ASSERT_ORDERED_ASCENDING(-INFINITY, LLONG_MAX);
+ ASSERT_ORDERED_ASCENDING(-INFINITY, 0LL);
+
+ ASSERT_ORDERED_DESCENDING(INFINITY, LLONG_MIN);
+ ASSERT_ORDERED_DESCENDING(INFINITY, LLONG_MAX);
+ ASSERT_ORDERED_DESCENDING(INFINITY, 0LL);
+
+ // NaN
+ ASSERT_ORDERED_ASCENDING(NAN, LLONG_MIN);
+ ASSERT_ORDERED_ASCENDING(NAN, LLONG_MAX);
+ ASSERT_ORDERED_ASCENDING(NAN, 0LL);
+
+ // Large values (note DBL_MIN is positive and near zero).
+ ASSERT_ORDERED_ASCENDING(-DBL_MAX, LLONG_MIN);
+
+ // Tests around LLONG_MIN
+ ASSERT_BIT_EQUALS((double)LLONG_MIN, -0x1.0p63);
+ ASSERT_ORDERED_SAME(-0x1.0p63, LLONG_MIN);
+ ASSERT_ORDERED_ASCENDING(-0x1.0p63, LLONG_MIN + 1);
+
+ XCTAssertLessThan(-0x1.0000000000001p63, -0x1.0p63);
+ ASSERT_ORDERED_ASCENDING(-0x1.0000000000001p63, LLONG_MIN);
+ ASSERT_ORDERED_DESCENDING(-0x1.FFFFFFFFFFFFFp62, LLONG_MIN);
+
+ // Tests around LLONG_MAX
+ // Note LLONG_MAX cannot be exactly represented by a double, so the system rounds it to the
+ // nearest double, which is 2^63. This number, in turn is larger than the maximum representable
+ // as a long.
+ ASSERT_BIT_EQUALS(0x1.0p63, (double)LLONG_MAX);
+ ASSERT_ORDERED_DESCENDING(0x1.0p63, LLONG_MAX);
+
+ // The largest value with an exactly long representation
+ XCTAssertEqual((long)0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFC00LL);
+ ASSERT_ORDERED_SAME(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFC00LL);
+
+ ASSERT_ORDERED_DESCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFB00LL);
+ ASSERT_ORDERED_DESCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFBFFLL);
+ ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFC01LL);
+ ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFD00LL);
+
+ ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFEp62, 0x7FFFFFFFFFFFFC00LL);
+
+ // Tests around MAX_SAFE_INTEGER
+ ASSERT_ORDERED_SAME(0x1.FFFFFFFFFFFFFp52, 0x1FFFFFFFFFFFFFLL);
+ ASSERT_ORDERED_DESCENDING(0x1.FFFFFFFFFFFFFp52, 0x1FFFFFFFFFFFFELL);
+ ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFEp52, 0x1FFFFFFFFFFFFFLL);
+ ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFFp52, 0x20000000000000LL);
+
+ // Tests around MIN_SAFE_INTEGER
+ ASSERT_ORDERED_SAME(-0x1.FFFFFFFFFFFFFp52, -0x1FFFFFFFFFFFFFLL);
+ ASSERT_ORDERED_ASCENDING(-0x1.FFFFFFFFFFFFFp52, -0x1FFFFFFFFFFFFELL);
+ ASSERT_ORDERED_DESCENDING(-0x1.FFFFFFFFFFFFEp52, -0x1FFFFFFFFFFFFFLL);
+ ASSERT_ORDERED_DESCENDING(-0x1.FFFFFFFFFFFFFp52, -0x20000000000000LL);
+
+ // Tests around zero.
+ ASSERT_ORDERED_SAME(-0.0, 0LL);
+ ASSERT_ORDERED_SAME(0.0, 0LL);
+
+ // The smallest representable positive value should be greater than zero
+ ASSERT_ORDERED_DESCENDING(DBL_MIN, 0LL);
+ ASSERT_ORDERED_ASCENDING(-DBL_MIN, 0LL);
+
+ // Note that 0x1.0p-1074 is a hex floating point literal representing the minimum subnormal
+ // number: <https://en.wikipedia.org/wiki/Denormal_number>.
+ double minSubNormal = 0x1.0p-1074;
+ ASSERT_ORDERED_DESCENDING(minSubNormal, 0LL);
+ ASSERT_ORDERED_ASCENDING(-minSubNormal, 0LL);
+
+ // Other sanity checks
+ ASSERT_ORDERED_ASCENDING(0.5, 1LL);
+ ASSERT_ORDERED_DESCENDING(0.5, 0LL);
+ ASSERT_ORDERED_ASCENDING(1.5, 2LL);
+ ASSERT_ORDERED_DESCENDING(1.5, 1LL);
+}
+
+@end
diff --git a/Firestore/Example/Tests/Util/FSTEventAccumulator.h b/Firestore/Example/Tests/Util/FSTEventAccumulator.h
new file mode 100644
index 0000000..ae5392c
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTEventAccumulator.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FIRDocumentSnapshot;
+@class FIRQuerySnapshot;
+@class XCTestCase;
+@class XCTestExpectation;
+
+NS_ASSUME_NONNULL_BEGIN
+
+typedef void (^FSTGenericEventHandler)(id _Nullable, NSError *error);
+
+@interface FSTEventAccumulator : NSObject
+
++ (instancetype)accumulatorForTest:(XCTestCase *)testCase;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+- (id)awaitEventWithName:(NSString *)name;
+
+- (NSArray<id> *)awaitEvents:(NSUInteger)events name:(NSString *)name;
+
+@property(nonatomic, strong, readonly) FSTGenericEventHandler handler;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Util/FSTEventAccumulator.m b/Firestore/Example/Tests/Util/FSTEventAccumulator.m
new file mode 100644
index 0000000..c7e5b41
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTEventAccumulator.m
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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.
+ */
+
+#import "FSTEventAccumulator.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Util/FSTAssert.h"
+
+#import "XCTestCase+Await.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTEventAccumulator ()
+- (instancetype)initForTest:(XCTestCase *)testCase NS_DESIGNATED_INITIALIZER;
+
+@property(nonatomic, weak, readonly) XCTestCase *testCase;
+
+@property(nonatomic, assign) NSUInteger maxEvents;
+@property(nonatomic, strong, nullable) XCTestExpectation *expectation;
+@end
+
+@implementation FSTEventAccumulator {
+ NSMutableArray<id> *_events;
+}
+
++ (instancetype)accumulatorForTest:(XCTestCase *)testCase {
+ return [[FSTEventAccumulator alloc] initForTest:testCase];
+}
+
+- (instancetype)initForTest:(XCTestCase *)testCase {
+ if (self = [super init]) {
+ _testCase = testCase;
+ _events = [NSMutableArray array];
+ }
+ return self;
+}
+
+- (NSArray<id> *)awaitEvents:(NSUInteger)events name:(NSString *)name {
+ @synchronized(self) {
+ FSTAssert(!self.expectation, @"Existing expectation still pending?");
+ self.expectation = [self.testCase expectationWithDescription:name];
+ self.maxEvents = self.maxEvents + events;
+ [self checkFulfilled];
+ }
+
+ // Don't await within @synchronized block to avoid deadlocking.
+ [self.testCase awaitExpectations];
+
+ return [_events subarrayWithRange:NSMakeRange(self.maxEvents - events, events)];
+}
+
+- (id)awaitEventWithName:(NSString *)name {
+ NSArray<id> *events = [self awaitEvents:1 name:name];
+ return events[0];
+}
+
+// Overrides the handler property
+- (void (^)(id _Nullable, NSError *))handler {
+ return ^void(id _Nullable value, NSError *error) {
+ // We can't store nil in the _events array, but these are still interesting to tests so store
+ // NSNull instead.
+ id event = value ? value : [NSNull null];
+
+ @synchronized(self) {
+ [_events addObject:event];
+ [self checkFulfilled];
+ }
+ };
+}
+
+- (void)checkFulfilled {
+ if (_events.count >= self.maxEvents) {
+ [self.expectation fulfill];
+ self.expectation = nil;
+ }
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Util/FSTHelpers.h b/Firestore/Example/Tests/Util/FSTHelpers.h
new file mode 100644
index 0000000..3facc9c
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTHelpers.h
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "API/FIRDocumentReference+Internal.h"
+#import "Core/FSTTypes.h"
+#import "Model/FSTDocumentDictionary.h"
+#import "Model/FSTDocumentKeySet.h"
+
+@class FIRGeoPoint;
+@class FSTDeleteMutation;
+@class FSTDeletedDocument;
+@class FSTDocument;
+@class FSTDocumentKeyReference;
+@class FSTDocumentSet;
+@class FSTFieldPath;
+@class FSTFieldValue;
+@class FSTLocalViewChanges;
+@class FSTPatchMutation;
+@class FSTQuery;
+@class FSTRemoteEvent;
+@class FSTResourceName;
+@class FSTResourcePath;
+@class FSTSetMutation;
+@class FSTSnapshotVersion;
+@class FSTSortOrder;
+@class FSTTargetChange;
+@class FSTTimestamp;
+@class FSTTransformMutation;
+@class FSTView;
+@class FSTViewSnapshot;
+@class FSTObjectValue;
+@protocol FSTFilter;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#if __cplusplus
+extern "C" {
+#endif
+
+#define FSTAssertIsKindOfClass(value, classType) \
+ do { \
+ XCTAssertEqualObjects([value class], [classType class]); \
+ } while (0);
+
+/**
+ * Asserts that the given NSSet of FSTDocumentKeys contains exactly the given expected keys.
+ * This is a macro instead of a method so that the failure shows up on the right line.
+ *
+ * @param actualSet An NSSet of FSTDocumentKeys.
+ * @param expectedArray A sorted array of keys that actualSet must be equal to (after converting
+ * to an array and sorting).
+ */
+#define FSTAssertEqualSets(actualSet, expectedArray) \
+ do { \
+ NSArray<FSTDocumentKey *> *actual = [(actualSet)allObjects]; \
+ actual = [actual sortedArrayUsingSelector:@selector(compare:)]; \
+ XCTAssertEqualObjects(actual, (expectedArray)); \
+ } while (0)
+
+/**
+ * Takes an array of "equality group" arrays and asserts that the compare: selector returns the
+ * same as compare: on the indexes of the "equality groups" (NSOrderedSame for items in the same
+ * group).
+ */
+#define FSTAssertComparisons(values) \
+ do { \
+ for (NSUInteger i = 0; i < [values count]; i++) { \
+ for (id left in values[i]) { \
+ for (NSUInteger j = 0; j < [values count]; j++) { \
+ for (id right in values[j]) { \
+ NSComparisonResult expected = [@(i) compare:@(j)]; \
+ NSComparisonResult result = [left compare:right]; \
+ NSComparisonResult inverseResult = [right compare:left]; \
+ XCTAssertEqual(result, expected, @"comparing %@ with %@ at (%lu, %lu)", left, right, \
+ i, j); \
+ XCTAssertEqual(inverseResult, -expected, @"comparing %@ with %@ at (%lu, %lu)", right, \
+ left, j, i); \
+ } \
+ } \
+ } \
+ } \
+ } while (0)
+
+/**
+ * Takes an array of "equality group" arrays and asserts that the isEqual: selector returns TRUE
+ * if-and-only-if items are in the same group.
+ *
+ * Additionally checks that the hash: selector returns the same value for items in the same group.
+ */
+#define FSTAssertEqualityGroups(values) \
+ do { \
+ for (NSUInteger i = 0; i < [values count]; i++) { \
+ for (id left in values[i]) { \
+ for (NSUInteger j = 0; j < [values count]; j++) { \
+ for (id right in values[j]) { \
+ if (i == j) { \
+ XCTAssertEqualObjects(left, right); \
+ XCTAssertEqual([left hash], [right hash], @"comparing hash of %@ with hash of %@", \
+ left, right); \
+ } else { \
+ XCTAssertNotEqualObjects(left, right); \
+ } \
+ } \
+ } \
+ } \
+ } \
+ } while (0)
+
+// Helper for validating API exceptions.
+#define FSTAssertThrows(expression, exceptionReason, ...) \
+ ({ \
+ BOOL __didThrow = NO; \
+ @try { \
+ (void)(expression); \
+ } @catch (NSException * exception) { \
+ __didThrow = YES; \
+ XCTAssertEqualObjects(exception.reason, exceptionReason); \
+ } \
+ XCTAssertTrue(__didThrow, ##__VA_ARGS__); \
+ })
+
+/** Creates a new FSTTimestamp from components. Note that year, month, and day are all one-based. */
+FSTTimestamp *FSTTestTimestamp(int year, int month, int day, int hour, int minute, int second);
+
+/** Creates a new NSDate from components. Note that year, month, and day are all one-based. */
+NSDate *FSTTestDate(int year, int month, int day, int hour, int minute, int second);
+
+/**
+ * Creates a new NSData from the var args of bytes, must be terminated with a negative byte
+ */
+NSData *FSTTestData(int bytes, ...);
+
+/** Creates a new GeoPoint from the latitude and longitude values */
+FIRGeoPoint *FSTTestGeoPoint(double latitude, double longitude);
+
+/**
+ * Creates a new NSDateComponents from components. Note that year, month, and day are all
+ * one-based.
+ */
+NSDateComponents *FSTTestDateComponents(
+ int year, int month, int day, int hour, int minute, int second);
+
+FSTFieldPath *FSTTestFieldPath(NSString *field);
+
+/** Wraps a plain value into an FSTFieldValue instance. */
+FSTFieldValue *FSTTestFieldValue(id _Nullable value);
+
+/** Wraps a NSDictionary value into an FSTObjectValue instance. */
+FSTObjectValue *FSTTestObjectValue(NSDictionary<NSString *, id> *data);
+
+/** A convenience method for creating document keys for tests. */
+FSTDocumentKey *FSTTestDocKey(NSString *path);
+
+/** A convenience method for creating a document key set for tests. */
+FSTDocumentKeySet *FSTTestDocKeySet(NSArray<FSTDocumentKey *> *keys);
+
+/** Allow tests to just use an int literal for versions. */
+typedef int64_t FSTTestSnapshotVersion;
+
+/** A convenience method for creating snapshot versions for tests. */
+FSTSnapshotVersion *FSTTestVersion(FSTTestSnapshotVersion version);
+
+/** A convenience method for creating docs for tests. */
+FSTDocument *FSTTestDoc(NSString *path,
+ FSTTestSnapshotVersion version,
+ NSDictionary<NSString *, id> *data,
+ BOOL hasMutations);
+
+/** A convenience method for creating deleted docs for tests. */
+FSTDeletedDocument *FSTTestDeletedDoc(NSString *path, FSTTestSnapshotVersion version);
+
+/** A convenience method for creating resource paths from a path string. */
+FSTResourcePath *FSTTestPath(NSString *path);
+
+/**
+ * A convenience method for creating a document reference from a path string.
+ */
+FSTDocumentKeyReference *FSTTestRef(NSString *projectID, NSString *databaseID, NSString *path);
+
+/** A convenience method for creating a query for the given path (without any other filters). */
+FSTQuery *FSTTestQuery(NSString *path);
+
+/**
+ * A convenience method to create a FSTFilter using a string representation for both field
+ * and operator (<, <=, ==, >=, >).
+ */
+id<FSTFilter> FSTTestFilter(NSString *field, NSString *op, id value);
+
+/** A convenience method for creating sort orders. */
+FSTSortOrder *FSTTestOrderBy(NSString *field, NSString *direction);
+
+/**
+ * Creates an NSComparator that will compare FSTDocuments by the given fieldPath string then by
+ * key.
+ */
+NSComparator FSTTestDocComparator(NSString *fieldPath);
+
+/**
+ * Creates a FSTDocumentSet based on the given comparator, initially containing the given
+ * documents.
+ */
+FSTDocumentSet *FSTTestDocSet(NSComparator comp, NSArray<FSTDocument *> *docs);
+
+/** Computes changes to the view with the docs and then applies them and returns the snapshot. */
+FSTViewSnapshot *_Nullable FSTTestApplyChanges(FSTView *view,
+ NSArray<FSTMaybeDocument *> *docs,
+ FSTTargetChange *_Nullable targetChange);
+
+/** Creates a set mutation for the document key at the given path. */
+FSTSetMutation *FSTTestSetMutation(NSString *path, NSDictionary<NSString *, id> *values);
+
+/** Creates a patch mutation for the document key at the given path. */
+FSTPatchMutation *FSTTestPatchMutation(NSString *path,
+ NSDictionary<NSString *, id> *values,
+ NSArray<FSTFieldPath *> *_Nullable updateMask);
+
+FSTTransformMutation *FSTTestTransformMutation(NSString *path,
+ NSArray<NSString *> *serverTimestampFields);
+
+/** Creates a delete mutation for the document key at the given path. */
+FSTDeleteMutation *FSTTestDeleteMutation(NSString *path);
+
+/** Converts a list of documents to a sorted map. */
+FSTMaybeDocumentDictionary *FSTTestDocUpdates(NSArray<FSTMaybeDocument *> *docs);
+
+/** Creates a remote event with changes to a document. */
+FSTRemoteEvent *FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc,
+ NSArray<NSNumber *> *updatedInTargets,
+ NSArray<NSNumber *> *removedFromTargets);
+
+/** Creates a test view changes. */
+FSTLocalViewChanges *FSTTestViewChanges(FSTQuery *query,
+ NSArray<NSString *> *addedKeys,
+ NSArray<NSString *> *removedKeys);
+
+/** Creates a resume token to match the given snapshot version. */
+NSData *_Nullable FSTTestResumeTokenFromSnapshotVersion(FSTTestSnapshotVersion watchSnapshot);
+
+#if __cplusplus
+} // extern "C"
+#endif
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Util/FSTHelpers.m b/Firestore/Example/Tests/Util/FSTHelpers.m
new file mode 100644
index 0000000..3b7f47f
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTHelpers.m
@@ -0,0 +1,348 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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.
+ */
+
+#import "FSTHelpers.h"
+
+#import "API/FIRFieldPath+Internal.h"
+#import "API/FSTUserDataConverter.h"
+#import "Core/FSTQuery.h"
+#import "Core/FSTSnapshotVersion.h"
+#import "Core/FSTTimestamp.h"
+#import "Core/FSTView.h"
+#import "Firestore/FIRFieldPath.h"
+#import "Firestore/FIRGeoPoint.h"
+#import "Local/FSTLocalViewChanges.h"
+#import "Local/FSTQueryData.h"
+#import "Model/FSTDatabaseID.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentKey.h"
+#import "Model/FSTDocumentSet.h"
+#import "Model/FSTFieldValue.h"
+#import "Model/FSTMutation.h"
+#import "Model/FSTPath.h"
+#import "Remote/FSTRemoteEvent.h"
+#import "Remote/FSTWatchChange.h"
+#import "Util/FSTAssert.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** A string sentinel that can be used with FSTTestPatchMutation() to mark a field for deletion. */
+static NSString *const kDeleteSentinel = @"<DELETE>";
+
+static const int kMicrosPerSec = 1000000;
+static const int kMillisPerSec = 1000;
+
+FSTTimestamp *FSTTestTimestamp(int year, int month, int day, int hour, int minute, int second) {
+ NSDate *date = FSTTestDate(year, month, day, hour, minute, second);
+ return [FSTTimestamp timestampWithDate:date];
+}
+
+NSDate *FSTTestDate(int year, int month, int day, int hour, int minute, int second) {
+ NSDateComponents *comps = FSTTestDateComponents(year, month, day, hour, minute, second);
+ return [[NSCalendar currentCalendar] dateFromComponents:comps];
+}
+
+NSData *FSTTestData(int bytes, ...) {
+ va_list args;
+ va_start(args, bytes); /* Initialize the argument list. */
+
+ NSMutableData *data = [NSMutableData data];
+
+ int next = bytes;
+ while (next >= 0) {
+ uint8_t byte = (uint8_t)next;
+ [data appendBytes:&byte length:1];
+ next = va_arg(args, int);
+ }
+
+ va_end(args);
+ return [data copy];
+}
+
+FIRGeoPoint *FSTTestGeoPoint(double latitude, double longitude) {
+ return [[FIRGeoPoint alloc] initWithLatitude:latitude longitude:longitude];
+}
+
+NSDateComponents *FSTTestDateComponents(
+ int year, int month, int day, int hour, int minute, int second) {
+ NSDateComponents *comps = [[NSDateComponents alloc] init];
+ comps.year = year;
+ comps.month = month;
+ comps.day = day;
+ comps.hour = hour;
+ comps.minute = minute;
+ comps.second = second;
+
+ // Force time zone to UTC to avoid these values changing due to daylight saving.
+ comps.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
+ return comps;
+}
+
+FSTFieldPath *FSTTestFieldPath(NSString *field) {
+ return [FIRFieldPath pathWithDotSeparatedString:field].internalValue;
+}
+
+FSTFieldValue *FSTTestFieldValue(id _Nullable value) {
+ FSTDatabaseID *databaseID =
+ [FSTDatabaseID databaseIDWithProject:@"project" database:kDefaultDatabaseID];
+ FSTUserDataConverter *converter =
+ [[FSTUserDataConverter alloc] initWithDatabaseID:databaseID
+ preConverter:^id _Nullable(id _Nullable input) {
+ return input;
+ }];
+ // HACK: We use parsedQueryValue: since it accepts scalars as well as arrays / objects, and
+ // our tests currently use FSTTestFieldValue() pretty generically so we don't know the intent.
+ return [converter parsedQueryValue:value];
+}
+
+FSTObjectValue *FSTTestObjectValue(NSDictionary<NSString *, id> *data) {
+ FSTFieldValue *wrapped = FSTTestFieldValue(data);
+ FSTCAssert([wrapped isKindOfClass:[FSTObjectValue class]], @"Unsupported value: %@", data);
+ return (FSTObjectValue *)wrapped;
+}
+
+FSTDocumentKey *FSTTestDocKey(NSString *path) {
+ return [FSTDocumentKey keyWithPathString:path];
+}
+
+FSTDocumentKeySet *FSTTestDocKeySet(NSArray<FSTDocumentKey *> *keys) {
+ FSTDocumentKeySet *result = [FSTDocumentKeySet keySet];
+ for (FSTDocumentKey *key in keys) {
+ result = [result setByAddingObject:key];
+ }
+ return result;
+}
+
+FSTSnapshotVersion *FSTTestVersion(FSTTestSnapshotVersion versionMicroseconds) {
+ int64_t seconds = versionMicroseconds / kMicrosPerSec;
+ int32_t nanos = (int32_t)(versionMicroseconds % kMicrosPerSec) * kMillisPerSec;
+
+ FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:seconds nanos:nanos];
+ return [FSTSnapshotVersion versionWithTimestamp:timestamp];
+}
+
+FSTDocument *FSTTestDoc(NSString *path,
+ FSTTestSnapshotVersion version,
+ NSDictionary<NSString *, id> *data,
+ BOOL hasMutations) {
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:path];
+ return [FSTDocument documentWithData:FSTTestObjectValue(data)
+ key:key
+ version:FSTTestVersion(version)
+ hasLocalMutations:hasMutations];
+}
+
+FSTDeletedDocument *FSTTestDeletedDoc(NSString *path, FSTTestSnapshotVersion version) {
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:path];
+ return [FSTDeletedDocument documentWithKey:key version:FSTTestVersion(version)];
+}
+
+static NSArray<NSString *> *FSTTestSplitPath(NSString *path) {
+ if ([path isEqualToString:@""]) {
+ return @[];
+ } else {
+ return [path componentsSeparatedByString:@"/"];
+ }
+}
+
+FSTResourcePath *FSTTestPath(NSString *path) {
+ return [FSTResourcePath pathWithSegments:FSTTestSplitPath(path)];
+}
+
+FSTDocumentKeyReference *FSTTestRef(NSString *projectID, NSString *database, NSString *path) {
+ FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:projectID database:database];
+ return [[FSTDocumentKeyReference alloc] initWithKey:FSTTestDocKey(path) databaseID:databaseID];
+}
+
+FSTQuery *FSTTestQuery(NSString *path) {
+ return [FSTQuery queryWithPath:FSTTestPath(path)];
+}
+
+id<FSTFilter> FSTTestFilter(NSString *field, NSString *opString, id value) {
+ FSTFieldPath *path = FSTTestFieldPath(field);
+ FSTRelationFilterOperator op;
+ if ([opString isEqualToString:@"<"]) {
+ op = FSTRelationFilterOperatorLessThan;
+ } else if ([opString isEqualToString:@"<="]) {
+ op = FSTRelationFilterOperatorLessThanOrEqual;
+ } else if ([opString isEqualToString:@"=="]) {
+ op = FSTRelationFilterOperatorEqual;
+ } else if ([opString isEqualToString:@">="]) {
+ op = FSTRelationFilterOperatorGreaterThanOrEqual;
+ } else if ([opString isEqualToString:@">"]) {
+ op = FSTRelationFilterOperatorGreaterThan;
+ } else {
+ FSTCFail(@"Unsupported operator type: %@", opString);
+ }
+
+ FSTFieldValue *data = FSTTestFieldValue(value);
+ if ([data isEqual:[FSTDoubleValue nanValue]]) {
+ FSTCAssert(op == FSTRelationFilterOperatorEqual, @"Must use == with NAN.");
+ return [[FSTNanFilter alloc] initWithField:path];
+ } else if ([data isEqual:[FSTNullValue nullValue]]) {
+ FSTCAssert(op == FSTRelationFilterOperatorEqual, @"Must use == with Null.");
+ return [[FSTNullFilter alloc] initWithField:path];
+ } else {
+ return [FSTRelationFilter filterWithField:path filterOperator:op value:data];
+ }
+}
+
+FSTSortOrder *FSTTestOrderBy(NSString *field, NSString *direction) {
+ FSTFieldPath *path = FSTTestFieldPath(field);
+ BOOL ascending;
+ if ([direction isEqualToString:@"asc"]) {
+ ascending = YES;
+ } else if ([direction isEqualToString:@"desc"]) {
+ ascending = NO;
+ } else {
+ FSTCFail(@"Unsupported direction: %@", direction);
+ }
+ return [FSTSortOrder sortOrderWithFieldPath:path ascending:ascending];
+}
+
+NSComparator FSTTestDocComparator(NSString *fieldPath) {
+ FSTQuery *query = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"docs" ]]]
+ queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(fieldPath)
+ ascending:YES]];
+ return [query comparator];
+}
+
+FSTDocumentSet *FSTTestDocSet(NSComparator comp, NSArray<FSTDocument *> *docs) {
+ FSTDocumentSet *docSet = [FSTDocumentSet documentSetWithComparator:comp];
+ for (FSTDocument *doc in docs) {
+ docSet = [docSet documentSetByAddingDocument:doc];
+ }
+ return docSet;
+}
+
+FSTSetMutation *FSTTestSetMutation(NSString *path, NSDictionary<NSString *, id> *values) {
+ return [[FSTSetMutation alloc] initWithKey:[FSTDocumentKey keyWithPathString:path]
+ value:FSTTestObjectValue(values)
+ precondition:[FSTPrecondition none]];
+}
+
+FSTPatchMutation *FSTTestPatchMutation(NSString *path,
+ NSDictionary<NSString *, id> *values,
+ NSArray<FSTFieldPath *> *_Nullable updateMask) {
+ BOOL merge = updateMask != nil;
+
+ __block FSTObjectValue *objectValue = [FSTObjectValue objectValue];
+ NSMutableArray<FSTFieldPath *> *fieldMaskPaths = [NSMutableArray array];
+ [values enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
+ FSTFieldPath *path = FSTTestFieldPath(key);
+ [fieldMaskPaths addObject:path];
+ if (![value isEqual:kDeleteSentinel]) {
+ FSTFieldValue *parsedValue = FSTTestFieldValue(value);
+ objectValue = [objectValue objectBySettingValue:parsedValue forPath:path];
+ }
+ }];
+
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPath:FSTTestPath(path)];
+ FSTFieldMask *mask = [[FSTFieldMask alloc] initWithFields:merge ? updateMask : fieldMaskPaths];
+ return [[FSTPatchMutation alloc] initWithKey:key
+ fieldMask:mask
+ value:objectValue
+ precondition:[FSTPrecondition preconditionWithExists:YES]];
+}
+
+// For now this only creates TransformMutations with server timestamps.
+FSTTransformMutation *FSTTestTransformMutation(NSString *path,
+ NSArray<NSString *> *serverTimestampFields) {
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPath:FSTTestPath(path)];
+ NSMutableArray<FSTFieldTransform *> *fieldTransforms = [NSMutableArray array];
+ for (NSString *field in serverTimestampFields) {
+ FSTFieldPath *fieldPath = FSTTestFieldPath(field);
+ id<FSTTransformOperation> transformOp = [FSTServerTimestampTransform serverTimestampTransform];
+ FSTFieldTransform *transform =
+ [[FSTFieldTransform alloc] initWithPath:fieldPath transform:transformOp];
+ [fieldTransforms addObject:transform];
+ }
+ return [[FSTTransformMutation alloc] initWithKey:key fieldTransforms:fieldTransforms];
+}
+
+FSTDeleteMutation *FSTTestDeleteMutation(NSString *path) {
+ return [[FSTDeleteMutation alloc] initWithKey:[FSTDocumentKey keyWithPathString:path]
+ precondition:[FSTPrecondition none]];
+}
+
+FSTMaybeDocumentDictionary *FSTTestDocUpdates(NSArray<FSTMaybeDocument *> *docs) {
+ FSTMaybeDocumentDictionary *updates = [FSTMaybeDocumentDictionary maybeDocumentDictionary];
+ for (FSTMaybeDocument *doc in docs) {
+ updates = [updates dictionaryBySettingObject:doc forKey:doc.key];
+ }
+ return updates;
+}
+
+FSTViewSnapshot *_Nullable FSTTestApplyChanges(FSTView *view,
+ NSArray<FSTMaybeDocument *> *docs,
+ FSTTargetChange *_Nullable targetChange) {
+ return [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(docs)]
+ targetChange:targetChange]
+ .snapshot;
+}
+
+FSTRemoteEvent *FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc,
+ NSArray<NSNumber *> *updatedInTargets,
+ NSArray<NSNumber *> *removedFromTargets) {
+ FSTDocumentWatchChange *change =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:updatedInTargets
+ removedTargetIDs:removedFromTargets
+ documentKey:doc.key
+ document:doc];
+ NSMutableDictionary<NSNumber *, FSTQueryData *> *listens = [NSMutableDictionary dictionary];
+ FSTQueryData *dummyQueryData = [FSTQueryData alloc];
+ for (NSNumber *targetID in updatedInTargets) {
+ listens[targetID] = dummyQueryData;
+ }
+ for (NSNumber *targetID in removedFromTargets) {
+ listens[targetID] = dummyQueryData;
+ }
+ NSMutableDictionary<NSNumber *, NSNumber *> *pending = [NSMutableDictionary dictionary];
+ FSTWatchChangeAggregator *aggregator =
+ [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:doc.version
+ listenTargets:listens
+ pendingTargetResponses:pending];
+ [aggregator addWatchChange:change];
+ return [aggregator remoteEvent];
+}
+
+/** Creates a resume token to match the given snapshot version. */
+NSData *_Nullable FSTTestResumeTokenFromSnapshotVersion(FSTTestSnapshotVersion snapshotVersion) {
+ if (snapshotVersion == 0) {
+ return nil;
+ }
+
+ NSString *snapshotString = [NSString stringWithFormat:@"snapshot-%" PRId64, snapshotVersion];
+ return [snapshotString dataUsingEncoding:NSUTF8StringEncoding];
+}
+
+FSTLocalViewChanges *FSTTestViewChanges(FSTQuery *query,
+ NSArray<NSString *> *addedKeys,
+ NSArray<NSString *> *removedKeys) {
+ FSTDocumentKeySet *added = [FSTDocumentKeySet keySet];
+ for (NSString *keyPath in addedKeys) {
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:keyPath];
+ added = [added setByAddingObject:key];
+ }
+ FSTDocumentKeySet *removed = [FSTDocumentKeySet keySet];
+ for (NSString *keyPath in removedKeys) {
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:keyPath];
+ removed = [removed setByAddingObject:key];
+ }
+ return [FSTLocalViewChanges changesForQuery:query addedKeys:added removedKeys:removed];
+}
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h
new file mode 100644
index 0000000..cefd669
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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.
+ */
+
+#import <Foundation/Foundation.h>
+#import <XCTest/XCTest.h>
+
+#import "XCTestCase+Await.h"
+
+@class FIRCollectionReference;
+@class FIRDocumentSnapshot;
+@class FIRDocumentReference;
+@class FIRQuerySnapshot;
+@class FIRFirestore;
+@class FIRFirestoreSettings;
+@class FIRQuery;
+@class FSTEventAccumulator;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTIntegrationTestCase : XCTestCase
+
+/** Returns the default Firestore project ID for testing. */
++ (NSString *)projectID;
+
+/** Returns a FirestoreSettings configured to use either hexa or the emulator. */
++ (FIRFirestoreSettings *)settings;
+
+/** Returns a new Firestore connected to the "test-db" project. */
+- (FIRFirestore *)firestore;
+
+/** Returns a new Firestore connected to the project with the given projectID. */
+- (FIRFirestore *)firestoreWithProjectID:(NSString *)projectID;
+
+/** Synchronously shuts down the given firestore. */
+- (void)shutdownFirestore:(FIRFirestore *)firestore;
+
+- (NSString *)documentPath;
+
+- (FIRDocumentReference *)documentRef;
+
+- (FIRCollectionReference *)collectionRef;
+
+- (FIRCollectionReference *)collectionRefWithDocuments:
+ (NSDictionary<NSString *, NSDictionary<NSString *, id> *> *)documents;
+
+- (void)writeAllDocuments:(NSDictionary<NSString *, NSDictionary<NSString *, id> *> *)documents
+ toCollection:(FIRCollectionReference *)collection;
+
+- (void)readerAndWriterOnDocumentRef:(void (^)(NSString *path,
+ FIRDocumentReference *readerRef,
+ FIRDocumentReference *writerRef))action;
+
+- (FIRDocumentSnapshot *)readDocumentForRef:(FIRDocumentReference *)ref;
+
+- (FIRQuerySnapshot *)readDocumentSetForRef:(FIRQuery *)query;
+
+- (void)writeDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary<NSString *, id> *)data;
+
+- (void)updateDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary<NSString *, id> *)data;
+
+- (void)deleteDocumentRef:(FIRDocumentReference *)ref;
+
+/**
+ * "Blocks" the current thread/run loop until the block returns YES.
+ * Should only be called on the main thread.
+ * The block is invoked frequently and in a loop (every couple of millseconds) to ensure fast
+ * test progress and make sure actions to be run on main thread are not blocked by this method.
+ */
+- (void)waitUntil:(BOOL (^)())predicate;
+
+@property(nonatomic, strong) FIRFirestore *db;
+@property(nonatomic, strong) FSTEventAccumulator *eventAccumulator;
+@end
+
+/** Converts the FIRQuerySnapshot to an NSArray containing the data of the documents in order. */
+NSArray<NSDictionary<NSString *, id> *> *FIRQuerySnapshotGetData(FIRQuerySnapshot *docs);
+
+/** Converts the FIRQuerySnapshot to an NSArray containing the document IDs in order. */
+NSArray<NSString *> *FIRQuerySnapshotGetIDs(FIRQuerySnapshot *docs);
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.m b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.m
new file mode 100644
index 0000000..87a78c3
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.m
@@ -0,0 +1,285 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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.
+ */
+
+@import Firestore;
+
+#import "FSTIntegrationTestCase.h"
+
+#import <FirebaseCommunity/FIRLogger.h>
+#import <GRPCClient/GRPCCall+ChannelArg.h>
+#import <GRPCClient/GRPCCall+Tests.h>
+
+#import "API/FIRFirestore+Internal.h"
+#import "Auth/FSTEmptyCredentialsProvider.h"
+#import "Local/FSTLevelDB.h"
+#import "Model/FSTDatabaseID.h"
+#import "Util/FSTDispatchQueue.h"
+#import "Util/FSTUtil.h"
+
+#import "FSTEventAccumulator.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTIntegrationTestCase {
+ NSMutableArray<FIRFirestore *> *_firestores;
+}
+
+- (void)setUp {
+ [super setUp];
+
+ [self clearPersistence];
+
+ _firestores = [NSMutableArray array];
+ self.db = [self firestore];
+ self.eventAccumulator = [FSTEventAccumulator accumulatorForTest:self];
+}
+
+- (void)tearDown {
+ @try {
+ for (FIRFirestore *firestore in _firestores) {
+ [self shutdownFirestore:firestore];
+ }
+ } @finally {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ [GRPCCall closeOpenConnections];
+#pragma clang diagnostic pop
+ _firestores = nil;
+ [super tearDown];
+ }
+}
+
+- (void)clearPersistence {
+ NSString *levelDBDir = [FSTLevelDB documentsDirectory];
+ NSError *error;
+ if (![[NSFileManager defaultManager] removeItemAtPath:levelDBDir error:&error]) {
+ // file not found is okay.
+ XCTAssertTrue(
+ [error.domain isEqualToString:NSCocoaErrorDomain] && error.code == NSFileNoSuchFileError,
+ @"Failed to clear LevelDB Persistence: %@", error);
+ }
+}
+
+- (FIRFirestore *)firestore {
+ return [self firestoreWithProjectID:[FSTIntegrationTestCase projectID]];
+}
+
++ (NSString *)projectID {
+ NSString *project = [[NSProcessInfo processInfo] environment][@"PROJECT_ID"];
+ if (!project) {
+ project = @"test-db";
+ }
+ return project;
+}
+
++ (FIRFirestoreSettings *)settings {
+ FIRFirestoreSettings *settings = [[FIRFirestoreSettings alloc] init];
+ NSString *host = [[NSProcessInfo processInfo] environment][@"DATASTORE_HOST"];
+ settings.sslEnabled = YES;
+ if (!host) {
+ // If host is nil, there is no GoogleService-Info.plist. Check if a hexa integration test
+ // configuration is configured. The first bundle location is used by bazel builds. The
+ // second is used for github clones.
+ host = @"localhost:8081";
+ settings.sslEnabled = YES;
+ NSString *certsPath =
+ [[NSBundle mainBundle] pathForResource:@"PlugIns/IntegrationTests.xctest/CAcert"
+ ofType:@"pem"];
+ if (certsPath == nil) {
+ certsPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"CAcert" ofType:@"pem"];
+ }
+ unsigned long long fileSize =
+ [[[NSFileManager defaultManager] attributesOfItemAtPath:certsPath error:nil] fileSize];
+
+ if (fileSize == 0) {
+ NSLog(
+ @"The cert is not properly configured. Make sure setup_integration_tests.py "
+ "has been run.");
+ }
+ [GRPCCall useTestCertsPath:certsPath testName:@"test_cert_2" forHost:host];
+ }
+ settings.host = host;
+ settings.persistenceEnabled = YES;
+ NSLog(@"Configured integration test for %@ with SSL: %@", settings.host,
+ settings.sslEnabled ? @"YES" : @"NO");
+ return settings;
+}
+
+- (FIRFirestore *)firestoreWithProjectID:(NSString *)projectID {
+ NSString *persistenceKey = [NSString stringWithFormat:@"db%lu", (unsigned long)_firestores.count];
+
+ FSTDispatchQueue *workerDispatchQueue = [FSTDispatchQueue
+ queueWith:dispatch_queue_create("com.google.firebase.firestore", DISPATCH_QUEUE_SERIAL)];
+
+ FSTEmptyCredentialsProvider *credentialsProvider = [[FSTEmptyCredentialsProvider alloc] init];
+
+ FIRSetLoggerLevel(FIRLoggerLevelDebug);
+ // HACK: FIRFirestore expects a non-nil app, but for tests we cheat.
+ FIRApp *app = nil;
+ FIRFirestore *firestore = [[FIRFirestore alloc] initWithProjectID:projectID
+ database:kDefaultDatabaseID
+ persistenceKey:persistenceKey
+ credentialsProvider:credentialsProvider
+ workerDispatchQueue:workerDispatchQueue
+ firebaseApp:app];
+
+ firestore.settings = [FSTIntegrationTestCase settings];
+
+ [_firestores addObject:firestore];
+ return firestore;
+}
+
+- (void)shutdownFirestore:(FIRFirestore *)firestore {
+ XCTestExpectation *shutdownCompletion = [self expectationWithDescription:@"shutdown"];
+ [firestore shutdownWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [shutdownCompletion fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (NSString *)documentPath {
+ return [@"test-collection/" stringByAppendingString:[FSTUtil autoID]];
+}
+
+- (FIRDocumentReference *)documentRef {
+ return [self.db documentWithPath:[self documentPath]];
+}
+
+- (FIRCollectionReference *)collectionRef {
+ NSString *collectionName = [@"test-collection-" stringByAppendingString:[FSTUtil autoID]];
+ return [self.db collectionWithPath:collectionName];
+}
+
+- (FIRCollectionReference *)collectionRefWithDocuments:
+ (NSDictionary<NSString *, NSDictionary<NSString *, id> *> *)documents {
+ FIRCollectionReference *collection = [self collectionRef];
+ // Use a different instance to write the documents
+ [self writeAllDocuments:documents
+ toCollection:[[self firestore] collectionWithPath:collection.path]];
+ return collection;
+}
+
+- (void)writeAllDocuments:(NSDictionary<NSString *, NSDictionary<NSString *, id> *> *)documents
+ toCollection:(FIRCollectionReference *)collection {
+ [documents enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSDictionary<NSString *, id> *value,
+ BOOL *stop) {
+ FIRDocumentReference *ref = [collection documentWithPath:key];
+ [self writeDocumentRef:ref data:value];
+ }];
+}
+
+- (void)readerAndWriterOnDocumentRef:(void (^)(NSString *path,
+ FIRDocumentReference *readerRef,
+ FIRDocumentReference *writerRef))action {
+ FIRFirestore *reader = self.db; // for clarity
+ FIRFirestore *writer = [self firestore];
+
+ NSString *path = [self documentPath];
+ FIRDocumentReference *readerRef = [reader documentWithPath:path];
+ FIRDocumentReference *writerRef = [writer documentWithPath:path];
+ action(path, readerRef, writerRef);
+}
+
+- (FIRDocumentSnapshot *)readDocumentForRef:(FIRDocumentReference *)ref {
+ __block FIRDocumentSnapshot *result;
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"getData"];
+ [ref getDocumentWithCompletion:^(FIRDocumentSnapshot *doc, NSError *_Nullable error) {
+ XCTAssertNil(error);
+ result = doc;
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+
+ return result;
+}
+
+- (FIRQuerySnapshot *)readDocumentSetForRef:(FIRQuery *)query {
+ __block FIRQuerySnapshot *result;
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"getData"];
+ [query getDocumentsWithCompletion:^(FIRQuerySnapshot *documentSet, NSError *error) {
+ XCTAssertNil(error);
+ result = documentSet;
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+
+ return result;
+}
+
+- (void)writeDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary<NSString *, id> *)data {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"setData"];
+ [ref setData:data
+ completion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)updateDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary<id, id> *)data {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"updateData"];
+ [ref updateData:data
+ completion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)deleteDocumentRef:(FIRDocumentReference *)ref {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"deleteDocument"];
+ [ref deleteDocumentWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)waitUntil:(BOOL (^)())predicate {
+ NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate];
+ double waitSeconds = [self defaultExpectationWaitSeconds];
+ while (!predicate() && ([NSDate timeIntervalSinceReferenceDate] - start < waitSeconds)) {
+ // This waits for the next event or until the 100ms timeout is reached
+ [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
+ beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
+ }
+ if (!predicate()) {
+ XCTFail(@"Timeout");
+ }
+}
+
+NSArray<NSDictionary<NSString *, id> *> *FIRQuerySnapshotGetData(FIRQuerySnapshot *docs) {
+ NSMutableArray<NSDictionary<NSString *, id> *> *result = [NSMutableArray array];
+ for (FIRDocumentSnapshot *doc in docs.documents) {
+ [result addObject:doc.data];
+ }
+ return result;
+}
+
+NSArray<NSString *> *FIRQuerySnapshotGetIDs(FIRQuerySnapshot *docs) {
+ NSMutableArray<NSString *> *result = [NSMutableArray array];
+ for (FIRDocumentSnapshot *doc in docs.documents) {
+ [result addObject:doc.documentID];
+ }
+ return result;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Util/FSTUtilTests.m b/Firestore/Example/Tests/Util/FSTUtilTests.m
new file mode 100644
index 0000000..998832d
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTUtilTests.m
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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.
+ */
+
+#import "Util/FSTUtil.h"
+
+#import <XCTest/XCTest.h>
+
+@interface FSTUtilTests : XCTestCase
+@end
+
+@implementation FSTUtilTests
+
+- (void)testAutoID {
+ NSString *autoID = [FSTUtil autoID];
+ XCTAssertEqual([autoID length], 20);
+ for (NSUInteger i = 0; i < 20; i++) {
+ unichar c = [autoID characterAtIndex:i];
+ XCTAssert(c >= ' ' && c <= '~', @"Should be printable ascii characters.");
+ }
+}
+
+@end
diff --git a/Firestore/Example/Tests/Util/XCTestCase+Await.h b/Firestore/Example/Tests/Util/XCTestCase+Await.h
new file mode 100644
index 0000000..9d575f9
--- /dev/null
+++ b/Firestore/Example/Tests/Util/XCTestCase+Await.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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.
+ */
+
+#import <XCTest/XCTest.h>
+
+@interface XCTestCase (Await)
+
+/**
+ * Await all outstanding expectations with a reasonable timeout, and if any of them fail, XCTFail
+ * the test.
+ */
+- (void)awaitExpectations;
+
+/**
+ * Returns a reasonable timeout for testing against Firestore.
+ */
+- (double)defaultExpectationWaitSeconds;
+
+@end
diff --git a/Firestore/Example/Tests/Util/XCTestCase+Await.m b/Firestore/Example/Tests/Util/XCTestCase+Await.m
new file mode 100644
index 0000000..e200c8c
--- /dev/null
+++ b/Firestore/Example/Tests/Util/XCTestCase+Await.m
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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.
+ */
+
+#import "XCTestCase+Await.h"
+
+#import <Foundation/Foundation.h>
+
+static const double kExpectationWaitSeconds = 10.0;
+
+@implementation XCTestCase (Await)
+
+- (void)awaitExpectations {
+ [self waitForExpectationsWithTimeout:kExpectationWaitSeconds
+ handler:^(NSError *_Nullable expectationError) {
+ if (expectationError) {
+ XCTFail(@"Error waiting for timeout: %@", expectationError);
+ }
+ }];
+}
+
+- (double)defaultExpectationWaitSeconds {
+ return kExpectationWaitSeconds;
+}
+
+@end