diff options
author | Gil <mcg@google.com> | 2017-10-03 08:55:22 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-10-03 08:55:22 -0700 |
commit | bde743ed25166a0b320ae157bfb1d68064f531c9 (patch) | |
tree | 4dd7525d9df32fa5dbdb721d4b0d4f9b87f5e884 /Firestore/Example/Tests/Util | |
parent | bf550507ffa8beee149383a5bf1e2363bccefbb4 (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.m | 105 | ||||
-rw-r--r-- | Firestore/Example/Tests/Util/FSTComparisonTests.m | 143 | ||||
-rw-r--r-- | Firestore/Example/Tests/Util/FSTEventAccumulator.h | 41 | ||||
-rw-r--r-- | Firestore/Example/Tests/Util/FSTEventAccumulator.m | 94 | ||||
-rw-r--r-- | Firestore/Example/Tests/Util/FSTHelpers.h | 258 | ||||
-rw-r--r-- | Firestore/Example/Tests/Util/FSTHelpers.m | 348 | ||||
-rw-r--r-- | Firestore/Example/Tests/Util/FSTIntegrationTestCase.h | 94 | ||||
-rw-r--r-- | Firestore/Example/Tests/Util/FSTIntegrationTestCase.m | 285 | ||||
-rw-r--r-- | Firestore/Example/Tests/Util/FSTUtilTests.m | 35 | ||||
-rw-r--r-- | Firestore/Example/Tests/Util/XCTestCase+Await.h | 32 | ||||
-rw-r--r-- | Firestore/Example/Tests/Util/XCTestCase+Await.m | 38 |
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 |