From 78d187bde32b05c285e8befc60a48bffb238d533 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Thu, 2 Apr 2026 00:20:50 -0500 Subject: [PATCH] Run tests during CI --- .github/workflows/objective-c-xcode.yml | 4 + CoreZen.xcodeproj/project.pbxproj | 36 ++- .../xcshareddata/xcschemes/CoreZen.xcscheme | 12 + CoreZenTests/CategoryTests.m | 92 ++++-- CoreZenTests/Classes/TestDTO.h | 14 + CoreZenTests/Classes/TestDTO.m | 18 ++ CoreZenTests/Classes/TestDomainObject.h | 12 + CoreZenTests/Classes/TestDomainObject.m | 20 ++ CoreZenTests/Classes/TestTable.h | 11 + CoreZenTests/Classes/TestTable.m | 71 +++++ CoreZenTests/CoreZenTests.m | 22 +- CoreZenTests/DatabaseTests.m | 184 ++++++++++- CoreZenTests/DomainTests.m | 216 +++++++++++++ CoreZenTests/NodeTests.m | 294 ++++++++++++++++++ CoreZenTests/ObjectCacheTests.m | 155 +++++++-- CoreZenTests/QueueTests.m | 169 ++++++++++ 16 files changed, 1252 insertions(+), 78 deletions(-) create mode 100644 CoreZenTests/Classes/TestDTO.h create mode 100644 CoreZenTests/Classes/TestDTO.m create mode 100644 CoreZenTests/Classes/TestDomainObject.h create mode 100644 CoreZenTests/Classes/TestDomainObject.m create mode 100644 CoreZenTests/Classes/TestTable.h create mode 100644 CoreZenTests/Classes/TestTable.m create mode 100644 CoreZenTests/DomainTests.m create mode 100644 CoreZenTests/NodeTests.m create mode 100644 CoreZenTests/QueueTests.m diff --git a/.github/workflows/objective-c-xcode.yml b/.github/workflows/objective-c-xcode.yml index ecf6035..de3d3d9 100644 --- a/.github/workflows/objective-c-xcode.yml +++ b/.github/workflows/objective-c-xcode.yml @@ -61,6 +61,10 @@ jobs: run: | xcodebuild build -scheme CoreZen -project CoreZen.xcodeproj + - name: Test + run: | + xcodebuild test -scheme CoreZen -project CoreZen.xcodeproj + - name: Analyze run: | xcodebuild analyze -scheme CoreZen -project CoreZen.xcodeproj diff --git a/CoreZen.xcodeproj/project.pbxproj b/CoreZen.xcodeproj/project.pbxproj index 7ef43a3..aeae587 100644 --- a/CoreZen.xcodeproj/project.pbxproj +++ b/CoreZen.xcodeproj/project.pbxproj @@ -151,6 +151,12 @@ 0D8B0E7E4B2D6D25D9EE6309 /* libavutil.60.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 37C6CE60E6FAE7A41E5758F2 /* libavutil.60.dylib */; }; A648515826063B25A687BEE2 /* libmpv.2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B85B4A23B3B9E64D362517 /* libmpv.2.dylib */; }; 7BBED1C0448E642D02061408 /* libswscale.9.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = AF2CEA40DF73E47475E7F86E /* libswscale.9.dylib */; }; + CC00000228860A0000000001 /* NodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC00000128860A0000000001 /* NodeTests.m */; }; + CC00000228860A0000000002 /* QueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC00000128860A0000000002 /* QueueTests.m */; }; + CC00000228860A0000000003 /* DomainTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC00000128860A0000000003 /* DomainTests.m */; }; + CC00000228860A0000000004 /* TestDTO.m in Sources */ = {isa = PBXBuildFile; fileRef = CC00000128860A0000000005 /* TestDTO.m */; }; + CC00000228860A0000000005 /* TestTable.m in Sources */ = {isa = PBXBuildFile; fileRef = CC00000128860A0000000007 /* TestTable.m */; }; + CC00000228860A0000000006 /* TestDomainObject.m in Sources */ = {isa = PBXBuildFile; fileRef = CC00000128860A0000000009 /* TestDomainObject.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -515,6 +521,15 @@ 9215CC53E9C2CED004BBB718 /* render.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = render.h; sourceTree = ""; }; 79D0AF2EFC89B99015EE993D /* render_gl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = render_gl.h; sourceTree = ""; }; 53170EA3BE67D827F0B5181C /* stream_cb.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = stream_cb.h; sourceTree = ""; }; + CC00000128860A0000000001 /* NodeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NodeTests.m; sourceTree = ""; }; + CC00000128860A0000000002 /* QueueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QueueTests.m; sourceTree = ""; }; + CC00000128860A0000000003 /* DomainTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DomainTests.m; sourceTree = ""; }; + CC00000128860A0000000004 /* TestDTO.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestDTO.h; sourceTree = ""; }; + CC00000128860A0000000005 /* TestDTO.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TestDTO.m; sourceTree = ""; }; + CC00000128860A0000000006 /* TestTable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestTable.h; sourceTree = ""; }; + CC00000128860A0000000007 /* TestTable.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TestTable.m; sourceTree = ""; }; + CC00000128860A0000000008 /* TestDomainObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestDomainObject.h; sourceTree = ""; }; + CC00000128860A0000000009 /* TestDomainObject.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TestDomainObject.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -586,9 +601,12 @@ children = ( 5350F8B52886079F00F8CA68 /* Classes */, 5309C3B82885A1DC00BC0AAE /* CoreZenTests.m */, + 5350F8B62886080500F8CA68 /* CategoryTests.m */, 5350F8A32886020500F8CA68 /* DatabaseTests.m */, + CC00000128860A0000000003 /* DomainTests.m */, + CC00000128860A0000000001 /* NodeTests.m */, 5350F8B32886074F00F8CA68 /* ObjectCacheTests.m */, - 5350F8B62886080500F8CA68 /* CategoryTests.m */, + CC00000128860A0000000002 /* QueueTests.m */, ); path = CoreZenTests; sourceTree = ""; @@ -726,8 +744,14 @@ 5350F8B52886079F00F8CA68 /* Classes */ = { isa = PBXGroup; children = ( + CC00000128860A0000000008 /* TestDomainObject.h */, + CC00000128860A0000000009 /* TestDomainObject.m */, + CC00000128860A0000000004 /* TestDTO.h */, + CC00000128860A0000000005 /* TestDTO.m */, 538E052C2885FB5400CE9DE7 /* TestIdentifiable.h */, 538E052D2885FB5400CE9DE7 /* TestIdentifiable.m */, + CC00000128860A0000000006 /* TestTable.h */, + CC00000128860A0000000007 /* TestTable.m */, ); path = Classes; sourceTree = ""; @@ -1284,10 +1308,16 @@ buildActionMask = 2147483647; files = ( 5309C3B92885A1DC00BC0AAE /* CoreZenTests.m in Sources */, - 5350F8A42886020500F8CA68 /* DatabaseTests.m in Sources */, - 538E052E2885FB5400CE9DE7 /* TestIdentifiable.m in Sources */, 5350F8B72886080500F8CA68 /* CategoryTests.m in Sources */, + 5350F8A42886020500F8CA68 /* DatabaseTests.m in Sources */, + CC00000228860A0000000003 /* DomainTests.m in Sources */, + CC00000228860A0000000001 /* NodeTests.m in Sources */, 5350F8B42886074F00F8CA68 /* ObjectCacheTests.m in Sources */, + CC00000228860A0000000002 /* QueueTests.m in Sources */, + CC00000228860A0000000004 /* TestDTO.m in Sources */, + CC00000228860A0000000006 /* TestDomainObject.m in Sources */, + 538E052E2885FB5400CE9DE7 /* TestIdentifiable.m in Sources */, + CC00000228860A0000000005 /* TestTable.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/CoreZen.xcodeproj/xcshareddata/xcschemes/CoreZen.xcscheme b/CoreZen.xcodeproj/xcshareddata/xcschemes/CoreZen.xcscheme index 6a2b37c..b4ab19f 100644 --- a/CoreZen.xcodeproj/xcshareddata/xcschemes/CoreZen.xcscheme +++ b/CoreZen.xcodeproj/xcshareddata/xcschemes/CoreZen.xcscheme @@ -29,6 +29,18 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" shouldAutocreateTestPlan = "YES"> + + + + + + + +@interface ZENTestDTO : ZENDataTransferObject + +@property (nonatomic, copy) NSString *name; + +- (instancetype)initWithIdentifier:(ZENIdentifier)identifier name:(NSString *)name; + +@end diff --git a/CoreZenTests/Classes/TestDTO.m b/CoreZenTests/Classes/TestDTO.m new file mode 100644 index 0000000..878bdfa --- /dev/null +++ b/CoreZenTests/Classes/TestDTO.m @@ -0,0 +1,18 @@ +// +// TestDTO.m +// CoreZenTests +// + +#import "TestDTO.h" + +@implementation ZENTestDTO + +- (instancetype)initWithIdentifier:(ZENIdentifier)identifier name:(NSString *)name { + self = [super initWithIdentifier:identifier]; + if (self) { + _name = [name copy]; + } + return self; +} + +@end diff --git a/CoreZenTests/Classes/TestDomainObject.h b/CoreZenTests/Classes/TestDomainObject.h new file mode 100644 index 0000000..b71cc7f --- /dev/null +++ b/CoreZenTests/Classes/TestDomainObject.h @@ -0,0 +1,12 @@ +// +// TestDomainObject.h +// CoreZenTests +// + +#import + +@interface ZENTestDomainObject : ZENDomainObject + +@property (nonatomic, copy, readonly) NSString *name; + +@end diff --git a/CoreZenTests/Classes/TestDomainObject.m b/CoreZenTests/Classes/TestDomainObject.m new file mode 100644 index 0000000..a16ad50 --- /dev/null +++ b/CoreZenTests/Classes/TestDomainObject.m @@ -0,0 +1,20 @@ +// +// TestDomainObject.m +// CoreZenTests +// + +#import "TestDomainObject.h" +#import "TestDTO.h" + +@implementation ZENTestDomainObject + +- (instancetype)initWithDTO:(ZENDataTransferObject *)dto { + self = [super initWithDTO:dto]; + if (self) { + ZENTestDTO *testDTO = (ZENTestDTO *)dto; + _name = [testDTO.name copy]; + } + return self; +} + +@end diff --git a/CoreZenTests/Classes/TestTable.h b/CoreZenTests/Classes/TestTable.h new file mode 100644 index 0000000..ff38759 --- /dev/null +++ b/CoreZenTests/Classes/TestTable.h @@ -0,0 +1,11 @@ +// +// TestTable.h +// CoreZenTests +// + +#import +#import + +@interface ZENTestTable : NSObject + +@end diff --git a/CoreZenTests/Classes/TestTable.m b/CoreZenTests/Classes/TestTable.m new file mode 100644 index 0000000..b5b835d --- /dev/null +++ b/CoreZenTests/Classes/TestTable.m @@ -0,0 +1,71 @@ +// +// TestTable.m +// CoreZenTests +// + +#import "TestTable.h" +#import "TestDTO.h" + +@import CoreZen; + +@implementation ZENTestTable + ++ (NSString *)tableName { + return @"test_objects"; +} + ++ (BOOL)updateSchema:(ZENDatabase *)database version:(NSUInteger)version { + if (version == 1) { + [database executeUpdate: + @"CREATE TABLE IF NOT EXISTS test_objects (" + "identifier INTEGER PRIMARY KEY, " + "name TEXT" + ");"]; + return YES; + } + return NO; +} + +- (BOOL)insertDTO:(ZENDataTransferObject *)dto database:(ZENDatabase *)database { + ZENTestDTO *testDTO = (ZENTestDTO *)dto; + return [database executeUpdate:@"INSERT INTO test_objects (identifier, name) VALUES (?, ?);" + withArgumentsInArray:@[@(testDTO.identifier), testDTO.name ?: [NSNull null]]]; +} + +- (BOOL)updateDTO:(ZENDataTransferObject *)dto database:(ZENDatabase *)database { + ZENTestDTO *testDTO = (ZENTestDTO *)dto; + return [database executeUpdate:@"UPDATE test_objects SET name = ? WHERE identifier = ?;" + withArgumentsInArray:@[testDTO.name ?: [NSNull null], @(testDTO.identifier)]]; +} + +- (BOOL)deleteByIdentifier:(ZENIdentifier)identifier database:(ZENDatabase *)database { + return [database executeUpdate:@"DELETE FROM test_objects WHERE identifier = ?;" + withArgumentsInArray:@[@(identifier)]]; +} + +- (ZENDataTransferObject *)dtoFromRow:(ZENResultSet *)row { + ZENIdentifier identifier = [row longLongIntForColumnIndex:0]; + NSString *name = [row stringForColumnIndex:1]; + return [[ZENTestDTO alloc] initWithIdentifier:identifier name:name]; +} + +- (ZENResultSet *)allRows:(ZENDatabase *)database { + return [database executeQuery:@"SELECT identifier, name FROM test_objects ORDER BY identifier;"]; +} + +- (ZENResultSet *)rowByIdentifier:(ZENIdentifier)identifier database:(ZENDatabase *)database { + return [database executeQuery:@"SELECT identifier, name FROM test_objects WHERE identifier = ?;" + withArgumentsInArray:@[@(identifier)]]; +} + +- (NSUInteger)countAllRows:(ZENDatabase *)database { + ZENResultSet *rs = [database executeQuery:@"SELECT COUNT(*) FROM test_objects;"]; + NSUInteger count = 0; + if ([rs next]) { + count = (NSUInteger)[rs longLongIntForColumnIndex:0]; + } + [rs close]; + return count; +} + +@end diff --git a/CoreZenTests/CoreZenTests.m b/CoreZenTests/CoreZenTests.m index 589487f..f47c915 100644 --- a/CoreZenTests/CoreZenTests.m +++ b/CoreZenTests/CoreZenTests.m @@ -7,30 +7,16 @@ #import +@import CoreZen; + @interface CoreZenTests : XCTestCase @end @implementation CoreZenTests -- (void)setUp { - // Put setup code here. This method is called before the invocation of each test method in the class. -} - -- (void)tearDown { - // Put teardown code here. This method is called after the invocation of each test method in the class. -} - -- (void)testExample { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. -} - -- (void)testPerformanceExample { - // This is an example of a performance test case. - [self measureBlock:^{ - // Put the code you want to measure the time of here. - }]; +- (void)testFrameworkLoads { + XCTAssertTrue(CoreZenVersionNumber > 0, @"CoreZen framework should report a version number"); } @end diff --git a/CoreZenTests/DatabaseTests.m b/CoreZenTests/DatabaseTests.m index 7f18ea3..83f925e 100644 --- a/CoreZenTests/DatabaseTests.m +++ b/CoreZenTests/DatabaseTests.m @@ -7,30 +7,194 @@ #import +@import CoreZen; + +#import "TestDTO.h" +#import "TestTable.h" + @interface DatabaseTests : XCTestCase +@property (nonatomic, strong) ZENDatabaseQueue *queue; + @end @implementation DatabaseTests - (void)setUp { - // Put setup code here. This method is called before the invocation of each test method in the class. + ZENSetLargestObjectIdentifier(0); + self.queue = [ZENDatabaseQueue databaseQueueInMemory]; + XCTAssertNotNil(self.queue); + + ZENDatabaseSchema *schema = [ZENDatabaseSchema schemaWithTableClasses:@[[ZENTestTable class]]]; + [self.queue transactionSync:^(ZENDatabase *database) { + [schema initializeDatabase:database]; + }]; } - (void)tearDown { - // Put teardown code here. This method is called after the invocation of each test method in the class. + [self.queue shutdown]; + self.queue = nil; +} + +#pragma mark - Schema + +- (void)testSchemaCreatesTable { + [self.queue fetchSync:^(ZENDatabase *database) { + ZENResultSet *rs = [database executeQuery: + @"SELECT name FROM sqlite_master WHERE type='table' AND name='test_objects';"]; + XCTAssertTrue([rs next], @"test_objects table should exist"); + XCTAssertEqualObjects([rs stringForColumnIndex:0], @"test_objects"); + [rs close]; + }]; +} + +- (void)testSchemaVersionIsSet { + [self.queue fetchSync:^(ZENDatabase *database) { + ZENResultSet *rs = [database executeQuery:@"PRAGMA user_version;"]; + XCTAssertTrue([rs next]); + long long version = [rs longLongIntForColumnIndex:0]; + XCTAssertEqual(version, 1, @"Schema version should be 1 after first migration"); + [rs close]; + }]; +} + +#pragma mark - Insert + +- (void)testInsertAndFetchDTO { + ZENTestTable *table = [ZENTestTable new]; + ZENIdentifier id1 = ZENGetNextObjectIdentifier(); + ZENTestDTO *dto = [[ZENTestDTO alloc] initWithIdentifier:id1 name:@"Alice"]; + + [self.queue transactionSync:^(ZENDatabase *database) { + BOOL result = [table insertDTO:dto database:database]; + XCTAssertTrue(result, @"Insert should succeed"); + }]; + + [self.queue fetchSync:^(ZENDatabase *database) { + ZENResultSet *rs = [table rowByIdentifier:id1 database:database]; + XCTAssertTrue([rs next], @"Should find the inserted row"); + ZENTestDTO *fetched = (ZENTestDTO *)[table dtoFromRow:rs]; + XCTAssertEqual(fetched.identifier, id1); + XCTAssertEqualObjects(fetched.name, @"Alice"); + [rs close]; + }]; +} + +#pragma mark - Update + +- (void)testUpdateDTO { + ZENTestTable *table = [ZENTestTable new]; + ZENIdentifier id1 = ZENGetNextObjectIdentifier(); + ZENTestDTO *dto = [[ZENTestDTO alloc] initWithIdentifier:id1 name:@"Bob"]; + + [self.queue transactionSync:^(ZENDatabase *database) { + [table insertDTO:dto database:database]; + }]; + + dto.name = @"Robert"; + [self.queue transactionSync:^(ZENDatabase *database) { + BOOL result = [table updateDTO:dto database:database]; + XCTAssertTrue(result, @"Update should succeed"); + }]; + + [self.queue fetchSync:^(ZENDatabase *database) { + ZENResultSet *rs = [table rowByIdentifier:id1 database:database]; + XCTAssertTrue([rs next]); + ZENTestDTO *fetched = (ZENTestDTO *)[table dtoFromRow:rs]; + XCTAssertEqualObjects(fetched.name, @"Robert"); + [rs close]; + }]; +} + +#pragma mark - Delete + +- (void)testDeleteByIdentifier { + ZENTestTable *table = [ZENTestTable new]; + ZENIdentifier id1 = ZENGetNextObjectIdentifier(); + ZENTestDTO *dto = [[ZENTestDTO alloc] initWithIdentifier:id1 name:@"Charlie"]; + + [self.queue transactionSync:^(ZENDatabase *database) { + [table insertDTO:dto database:database]; + }]; + + [self.queue transactionSync:^(ZENDatabase *database) { + BOOL result = [table deleteByIdentifier:id1 database:database]; + XCTAssertTrue(result, @"Delete should succeed"); + }]; + + [self.queue fetchSync:^(ZENDatabase *database) { + ZENResultSet *rs = [table rowByIdentifier:id1 database:database]; + XCTAssertFalse([rs next], @"Deleted row should not be found"); + [rs close]; + }]; } -- (void)testExample { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. +#pragma mark - Count + +- (void)testCountAllRows { + ZENTestTable *table = [ZENTestTable new]; + + [self.queue transactionSync:^(ZENDatabase *database) { + for (int i = 0; i < 5; i++) { + ZENIdentifier ident = ZENGetNextObjectIdentifier(); + NSString *name = [NSString stringWithFormat:@"Item %d", i]; + ZENTestDTO *dto = [[ZENTestDTO alloc] initWithIdentifier:ident name:name]; + [table insertDTO:dto database:database]; + } + }]; + + [self.queue fetchSync:^(ZENDatabase *database) { + NSUInteger count = [table countAllRows:database]; + XCTAssertEqual(count, 5u, @"Should have 5 rows"); + }]; } -- (void)testPerformanceExample { - // This is an example of a performance test case. - [self measureBlock:^{ - // Put the code you want to measure the time of here. - }]; +#pragma mark - All Rows + +- (void)testAllRowsReturnsAllInsertedObjects { + ZENTestTable *table = [ZENTestTable new]; + NSArray *names = @[@"X", @"Y", @"Z"]; + + [self.queue transactionSync:^(ZENDatabase *database) { + for (NSString *name in names) { + ZENIdentifier ident = ZENGetNextObjectIdentifier(); + ZENTestDTO *dto = [[ZENTestDTO alloc] initWithIdentifier:ident name:name]; + [table insertDTO:dto database:database]; + } + }]; + + [self.queue fetchSync:^(ZENDatabase *database) { + ZENResultSet *rs = [table allRows:database]; + NSMutableArray *fetchedNames = [NSMutableArray new]; + while ([rs next]) { + ZENTestDTO *dto = (ZENTestDTO *)[table dtoFromRow:rs]; + [fetchedNames addObject:dto.name]; + } + [rs close]; + XCTAssertEqualObjects(fetchedNames, names, + @"Should fetch all inserted names in order"); + }]; +} + +#pragma mark - Async Operations + +- (void)testTransactionAsync { + XCTestExpectation *expectation = [self expectationWithDescription:@"async transaction"]; + ZENTestTable *table = [ZENTestTable new]; + ZENIdentifier id1 = ZENGetNextObjectIdentifier(); + ZENTestDTO *dto = [[ZENTestDTO alloc] initWithIdentifier:id1 name:@"Async"]; + + [self.queue transactionAsync:^(ZENDatabase *database) { + [table insertDTO:dto database:database]; + }]; + + [self.queue fetchAsync:^(ZENDatabase *database) { + NSUInteger count = [table countAllRows:database]; + XCTAssertEqual(count, 1u); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; } @end diff --git a/CoreZenTests/DomainTests.m b/CoreZenTests/DomainTests.m new file mode 100644 index 0000000..cd28aa0 --- /dev/null +++ b/CoreZenTests/DomainTests.m @@ -0,0 +1,216 @@ +// +// DomainTests.m +// CoreZenTests +// + +#import + +@import CoreZen; + +#import "TestDTO.h" +#import "TestTable.h" +#import "TestDomainObject.h" + +@interface DomainTests : XCTestCase + +@property (nonatomic, strong) ZENDatabaseQueue *queue; + +@end + +@implementation DomainTests + +- (void)setUp { + ZENSetLargestObjectIdentifier(0); + self.queue = [ZENDatabaseQueue databaseQueueInMemory]; + + ZENDatabaseSchema *schema = [ZENDatabaseSchema schemaWithTableClasses:@[[ZENTestTable class]]]; + [self.queue transactionSync:^(ZENDatabase *database) { + [schema initializeDatabase:database]; + }]; +} + +- (void)tearDown { + [self.queue shutdown]; + self.queue = nil; +} + +#pragma mark - DataTransferObject + +- (void)testDTODefaultIdentifier { + ZENDataTransferObject *dto = [[ZENDataTransferObject alloc] init]; + XCTAssertEqual(dto.identifier, ZENInvalidIdentifier); +} + +- (void)testDTOWithIdentifier { + ZENDataTransferObject *dto = [[ZENDataTransferObject alloc] initWithIdentifier:42]; + XCTAssertEqual(dto.identifier, 42); +} + +- (void)testTestDTOWithName { + ZENTestDTO *dto = [[ZENTestDTO alloc] initWithIdentifier:1 name:@"Test"]; + XCTAssertEqual(dto.identifier, 1); + XCTAssertEqualObjects(dto.name, @"Test"); +} + +#pragma mark - DomainObject + +- (void)testDomainObjectCreationFromDTO { + ZENTestDTO *dto = [[ZENTestDTO alloc] initWithIdentifier:10 name:@"Hello"]; + ZENTestDomainObject *obj = [[ZENTestDomainObject alloc] initWithDTO:dto]; + + XCTAssertEqual(obj.identifier, 10); + XCTAssertEqualObjects(obj.name, @"Hello"); + XCTAssertEqual(obj.basicDTO, dto); +} + +- (void)testDomainObjectIdentifierPassthrough { + ZENIdentifier id1 = ZENGetNextObjectIdentifier(); + ZENTestDTO *dto = [[ZENTestDTO alloc] initWithIdentifier:id1 name:@"PassThrough"]; + ZENDomainObject *obj = [[ZENDomainObject alloc] initWithDTO:dto]; + + XCTAssertEqual(obj.identifier, id1); +} + +#pragma mark - ObjectRepository Integration + +- (void)testRepositoryAddAndFetch { + XCTestExpectation *expectation = [self expectationWithDescription:@"add and fetch"]; + + ZENObjectRepository *repo = [[ZENObjectRepository alloc] + initWithDataModel:nil + tableClass:[ZENTestTable class] + databaseQueue:self.queue + domainObjectEmbryo:[ZENTestDomainObject class] + cacheType:ZENObjectRepositoryCacheType_Strong]; + + ZENIdentifier id1 = ZENGetNextObjectIdentifier(); + ZENTestDTO *dto = [[ZENTestDTO alloc] initWithIdentifier:id1 name:@"RepoTest"]; + ZENTestDomainObject *obj = [[ZENTestDomainObject alloc] initWithDTO:dto]; + + [repo addObject:obj completion:^{ + [repo fetchObjectByIdentifier:id1 completion:^(NSArray *fetchedObjects) { + XCTAssertEqual(fetchedObjects.count, 1u); + ZENTestDomainObject *fetched = fetchedObjects.firstObject; + XCTAssertEqual(fetched.identifier, id1); + [expectation fulfill]; + }]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testRepositoryDeleteObject { + XCTestExpectation *expectation = [self expectationWithDescription:@"delete"]; + + ZENObjectRepository *repo = [[ZENObjectRepository alloc] + initWithDataModel:nil + tableClass:[ZENTestTable class] + databaseQueue:self.queue + domainObjectEmbryo:[ZENTestDomainObject class] + cacheType:ZENObjectRepositoryCacheType_Strong]; + + ZENIdentifier id1 = ZENGetNextObjectIdentifier(); + ZENTestDTO *dto = [[ZENTestDTO alloc] initWithIdentifier:id1 name:@"ToDelete"]; + ZENTestDomainObject *obj = [[ZENTestDomainObject alloc] initWithDTO:dto]; + + [repo addObject:obj completion:^{ + [repo deleteObject:obj completion:^{ + [repo countAllObjects:^(NSUInteger count) { + XCTAssertEqual(count, 0u); + [expectation fulfill]; + }]; + }]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testRepositoryCountAllObjects { + XCTestExpectation *expectation = [self expectationWithDescription:@"count"]; + + ZENObjectRepository *repo = [[ZENObjectRepository alloc] + initWithDataModel:nil + tableClass:[ZENTestTable class] + databaseQueue:self.queue + domainObjectEmbryo:[ZENTestDomainObject class] + cacheType:ZENObjectRepositoryCacheType_Strong]; + + ZENIdentifier id1 = ZENGetNextObjectIdentifier(); + ZENIdentifier id2 = ZENGetNextObjectIdentifier(); + ZENTestDTO *dto1 = [[ZENTestDTO alloc] initWithIdentifier:id1 name:@"One"]; + ZENTestDTO *dto2 = [[ZENTestDTO alloc] initWithIdentifier:id2 name:@"Two"]; + + [repo addObject:[[ZENTestDomainObject alloc] initWithDTO:dto1] completion:^{ + [repo addObject:[[ZENTestDomainObject alloc] initWithDTO:dto2] completion:^{ + [repo countAllObjects:^(NSUInteger count) { + XCTAssertEqual(count, 2u); + [expectation fulfill]; + }]; + }]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testRepositoryFetchAllObjects { + XCTestExpectation *expectation = [self expectationWithDescription:@"fetch all"]; + + ZENObjectRepository *repo = [[ZENObjectRepository alloc] + initWithDataModel:nil + tableClass:[ZENTestTable class] + databaseQueue:self.queue + domainObjectEmbryo:[ZENTestDomainObject class] + cacheType:ZENObjectRepositoryCacheType_Strong]; + + ZENIdentifier id1 = ZENGetNextObjectIdentifier(); + ZENIdentifier id2 = ZENGetNextObjectIdentifier(); + ZENTestDTO *dto1 = [[ZENTestDTO alloc] initWithIdentifier:id1 name:@"Alpha"]; + ZENTestDTO *dto2 = [[ZENTestDTO alloc] initWithIdentifier:id2 name:@"Beta"]; + + [repo addObject:[[ZENTestDomainObject alloc] initWithDTO:dto1] completion:^{ + [repo addObject:[[ZENTestDomainObject alloc] initWithDTO:dto2] completion:^{ + [repo fetchAllObjects:^(NSArray *fetchedObjects) { + XCTAssertEqual(fetchedObjects.count, 2u); + [expectation fulfill]; + }]; + }]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testRepositoryUpdateObject { + XCTestExpectation *expectation = [self expectationWithDescription:@"update"]; + + ZENObjectRepository *repo = [[ZENObjectRepository alloc] + initWithDataModel:nil + tableClass:[ZENTestTable class] + databaseQueue:self.queue + domainObjectEmbryo:[ZENTestDomainObject class] + cacheType:ZENObjectRepositoryCacheType_Strong]; + + ZENIdentifier id1 = ZENGetNextObjectIdentifier(); + ZENTestDTO *dto = [[ZENTestDTO alloc] initWithIdentifier:id1 name:@"Original"]; + ZENTestDomainObject *obj = [[ZENTestDomainObject alloc] initWithDTO:dto]; + + [repo addObject:obj completion:^{ + // Mutate the DTO's name and push the update through the repository + dto.name = @"Updated"; + [repo updateObject:obj completion:^{ + // Verify the database row was changed by reading it directly + [self.queue fetchSync:^(ZENDatabase *database) { + ZENTestTable *table = [ZENTestTable new]; + ZENResultSet *rs = [table rowByIdentifier:id1 database:database]; + XCTAssertTrue([rs next]); + ZENTestDTO *fetched = (ZENTestDTO *)[table dtoFromRow:rs]; + XCTAssertEqualObjects(fetched.name, @"Updated"); + [rs close]; + [expectation fulfill]; + }]; + }]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +@end diff --git a/CoreZenTests/NodeTests.m b/CoreZenTests/NodeTests.m new file mode 100644 index 0000000..37d0cfd --- /dev/null +++ b/CoreZenTests/NodeTests.m @@ -0,0 +1,294 @@ +// +// NodeTests.m +// CoreZenTests +// + +#import + +@import CoreZen; + +@interface NodeTests : XCTestCase + +@end + +@implementation NodeTests + +#pragma mark - Initialization + +- (void)testInitWithName { + ZENNode *node = [[ZENNode alloc] initWithName:@"root"]; + XCTAssertNotNil(node); + XCTAssertEqualObjects(node.name, @"root"); + XCTAssertEqual(node.size, 0); + XCTAssertNotNil(node.nodeID); +} + +- (void)testInitWithNameAndSize { + ZENNode *node = [[ZENNode alloc] initWithName:@"file" size:1024]; + XCTAssertEqualObjects(node.name, @"file"); + XCTAssertEqual(node.size, 1024); +} + +- (void)testUniqueNodeIDs { + ZENNode *a = [[ZENNode alloc] initWithName:@"a"]; + ZENNode *b = [[ZENNode alloc] initWithName:@"b"]; + XCTAssertNotEqualObjects(a.nodeID, b.nodeID); +} + +#pragma mark - Parent / Child + +- (void)testAddChildSetsParent { + ZENNode *parent = [[ZENNode alloc] initWithName:@"parent"]; + ZENNode *child = [[ZENNode alloc] initWithName:@"child"]; + [parent addChildNode:child]; + + XCTAssertEqual(child.parent, parent); + XCTAssertEqual(parent.childCount, 1u); + XCTAssertFalse(parent.isChildless); +} + +- (void)testRemoveChildClearsParent { + ZENNode *parent = [[ZENNode alloc] initWithName:@"parent"]; + ZENNode *child = [[ZENNode alloc] initWithName:@"child"]; + [parent addChildNode:child]; + [parent removeChildNode:child]; + + XCTAssertNil(child.parent); + XCTAssertEqual(parent.childCount, 0u); + XCTAssertTrue(parent.isChildless); +} + +- (void)testAddChildTransfersFromOldParent { + ZENNode *parentA = [[ZENNode alloc] initWithName:@"A"]; + ZENNode *parentB = [[ZENNode alloc] initWithName:@"B"]; + ZENNode *child = [[ZENNode alloc] initWithName:@"child"]; + + [parentA addChildNode:child]; + XCTAssertEqual(parentA.childCount, 1u); + + [parentB addChildNode:child]; + XCTAssertEqual(parentA.childCount, 0u); + XCTAssertEqual(parentB.childCount, 1u); + XCTAssertEqual(child.parent, parentB); +} + +- (void)testAddingSameChildTwiceIsNoOp { + ZENNode *parent = [[ZENNode alloc] initWithName:@"parent"]; + ZENNode *child = [[ZENNode alloc] initWithName:@"child"]; + [parent addChildNode:child]; + [parent addChildNode:child]; + + XCTAssertEqual(parent.childCount, 1u); +} + +#pragma mark - Root Node + +- (void)testRootNodeSingleLevel { + ZENNode *root = [[ZENNode alloc] initWithName:@"root"]; + XCTAssertEqual([root rootNode], root); +} + +- (void)testRootNodeMultiLevel { + ZENNode *root = [[ZENNode alloc] initWithName:@"root"]; + ZENNode *mid = [[ZENNode alloc] initWithName:@"mid"]; + ZENNode *leaf = [[ZENNode alloc] initWithName:@"leaf"]; + [root addChildNode:mid]; + [mid addChildNode:leaf]; + + XCTAssertEqual([leaf rootNode], root); + XCTAssertEqual([mid rootNode], root); +} + +#pragma mark - Size Propagation + +- (void)testSizePropagatesOnAdd { + ZENNode *root = [[ZENNode alloc] initWithName:@"root"]; + ZENNode *childA = [[ZENNode alloc] initWithName:@"a" size:100]; + ZENNode *childB = [[ZENNode alloc] initWithName:@"b" size:200]; + + [root addChildNode:childA]; + XCTAssertEqual(root.size, 100); + + [root addChildNode:childB]; + XCTAssertEqual(root.size, 300); +} + +- (void)testSizeReducesOnRemove { + ZENNode *root = [[ZENNode alloc] initWithName:@"root"]; + ZENNode *child = [[ZENNode alloc] initWithName:@"child" size:50]; + [root addChildNode:child]; + [root removeChildNode:child]; + + XCTAssertEqual(root.size, 0); +} + +- (void)testSizePropagatesUpTree { + ZENNode *root = [[ZENNode alloc] initWithName:@"root"]; + ZENNode *mid = [[ZENNode alloc] initWithName:@"mid"]; + [root addChildNode:mid]; + + ZENNode *leaf = [[ZENNode alloc] initWithName:@"leaf" size:42]; + [mid addChildNode:leaf]; + + XCTAssertEqual(mid.size, 42); + XCTAssertEqual(root.size, 42); +} + +#pragma mark - Counting + +- (void)testCountLeaves { + // root + // / \ + // mid leaf1 + // / \ + // leaf2 leaf3 + ZENNode *root = [[ZENNode alloc] initWithName:@"root"]; + ZENNode *mid = [[ZENNode alloc] initWithName:@"mid"]; + ZENNode *leaf1 = [[ZENNode alloc] initWithName:@"leaf1"]; + ZENNode *leaf2 = [[ZENNode alloc] initWithName:@"leaf2"]; + ZENNode *leaf3 = [[ZENNode alloc] initWithName:@"leaf3"]; + + [root addChildNode:mid]; + [root addChildNode:leaf1]; + [mid addChildNode:leaf2]; + [mid addChildNode:leaf3]; + + XCTAssertEqual([root countLeaves], 3u); +} + +- (void)testCountChildrenAndDescendants { + ZENNode *root = [[ZENNode alloc] initWithName:@"root"]; + ZENNode *mid = [[ZENNode alloc] initWithName:@"mid"]; + ZENNode *leaf1 = [[ZENNode alloc] initWithName:@"leaf1"]; + ZENNode *leaf2 = [[ZENNode alloc] initWithName:@"leaf2"]; + + [root addChildNode:mid]; + [root addChildNode:leaf1]; + [mid addChildNode:leaf2]; + + // Descendants: mid, leaf1, leaf2 = 3 + XCTAssertEqual([root countChildrenAndDescendants], 3u); +} + +#pragma mark - Enumeration + +- (void)testEnumerateChildren { + ZENNode *root = [[ZENNode alloc] initWithName:@"root"]; + ZENNode *a = [[ZENNode alloc] initWithName:@"a"]; + ZENNode *b = [[ZENNode alloc] initWithName:@"b"]; + [root addChildNode:a]; + [root addChildNode:b]; + + NSMutableArray *names = [NSMutableArray new]; + [root enumerateChildrenWithBlock:^(ZENNode *node, NSUInteger index, BOOL *stop) { + [names addObject:node.name]; + }]; + + NSArray *expected = @[@"a", @"b"]; + XCTAssertEqualObjects(names, expected); +} + +- (void)testEnumerateChildrenStopFlag { + ZENNode *root = [[ZENNode alloc] initWithName:@"root"]; + [root addChildNode:[[ZENNode alloc] initWithName:@"a"]]; + [root addChildNode:[[ZENNode alloc] initWithName:@"b"]]; + [root addChildNode:[[ZENNode alloc] initWithName:@"c"]]; + + NSMutableArray *names = [NSMutableArray new]; + [root enumerateChildrenWithBlock:^(ZENNode *node, NSUInteger index, BOOL *stop) { + [names addObject:node.name]; + if (index == 0) *stop = YES; + }]; + + XCTAssertEqual(names.count, 1u); +} + +- (void)testEnumerateParents { + ZENNode *root = [[ZENNode alloc] initWithName:@"root"]; + ZENNode *mid = [[ZENNode alloc] initWithName:@"mid"]; + ZENNode *leaf = [[ZENNode alloc] initWithName:@"leaf"]; + [root addChildNode:mid]; + [mid addChildNode:leaf]; + + NSMutableArray *names = [NSMutableArray new]; + [leaf enumerateParentsWithBlock:^(ZENNode *node, NSUInteger index, BOOL *stop) { + [names addObject:node.name]; + }]; + + NSArray *expected = @[@"mid", @"root"]; + XCTAssertEqualObjects(names, expected); +} + +- (void)testDepthFirstEnumeration { + // root + // / \ + // a b + // / \ + // c d + ZENNode *root = [[ZENNode alloc] initWithName:@"root"]; + ZENNode *a = [[ZENNode alloc] initWithName:@"a"]; + ZENNode *b = [[ZENNode alloc] initWithName:@"b"]; + ZENNode *c = [[ZENNode alloc] initWithName:@"c"]; + ZENNode *d = [[ZENNode alloc] initWithName:@"d"]; + [root addChildNode:a]; + [root addChildNode:b]; + [a addChildNode:c]; + [a addChildNode:d]; + + NSMutableArray *names = [NSMutableArray new]; + [root enumerateDepthFirstUsingBlock:^(ZENNode *node, NSUInteger index, BOOL *stop) { + [names addObject:node.name]; + }]; + + // Post-order: c, d, a, b + NSArray *expected = @[@"c", @"d", @"a", @"b"]; + XCTAssertEqualObjects(names, expected); +} + +- (void)testBreadthFirstEnumeration { + // root + // / \ + // a b + // / \ + // c d + ZENNode *root = [[ZENNode alloc] initWithName:@"root"]; + ZENNode *a = [[ZENNode alloc] initWithName:@"a"]; + ZENNode *b = [[ZENNode alloc] initWithName:@"b"]; + ZENNode *c = [[ZENNode alloc] initWithName:@"c"]; + ZENNode *d = [[ZENNode alloc] initWithName:@"d"]; + [root addChildNode:a]; + [root addChildNode:b]; + [a addChildNode:c]; + [a addChildNode:d]; + + NSMutableArray *names = [NSMutableArray new]; + [root enumerateBreadthFirstUsingBlock:^(ZENNode *node, NSUInteger index, BOOL *stop) { + [names addObject:node.name]; + }]; + + NSArray *expected = @[@"a", @"b", @"c", @"d"]; + XCTAssertEqualObjects(names, expected); +} + +#pragma mark - Copy + +- (void)testCopyCreatesIndependentTree { + ZENNode *root = [[ZENNode alloc] initWithName:@"root"]; + ZENNode *child = [[ZENNode alloc] initWithName:@"child" size:10]; + [root addChildNode:child]; + + ZENNode *copy = [root copy]; + + XCTAssertEqualObjects(copy.name, @"root"); + XCTAssertEqual(copy.childCount, 1u); + XCTAssertEqual(copy.size, 10); + + // Copy should be independent + XCTAssertNotEqualObjects(copy.nodeID, root.nodeID); + + ZENNode *copiedChild = copy.children.firstObject; + XCTAssertEqualObjects(copiedChild.name, @"child"); + XCTAssertNotEqual(copiedChild, child); +} + +@end diff --git a/CoreZenTests/ObjectCacheTests.m b/CoreZenTests/ObjectCacheTests.m index 2be600e..ae29101 100644 --- a/CoreZenTests/ObjectCacheTests.m +++ b/CoreZenTests/ObjectCacheTests.m @@ -19,58 +19,155 @@ @interface ObjectCacheTests : XCTestCase @implementation ObjectCacheTests - (void)setUp { - // Put setup code here. This method is called before the invocation of each test method in the class. - self.cache = [ZENObjectCache weakObjectCache]; - XCTAssert(self.cache); - + XCTAssertNotNil(self.cache); ZENSetLargestObjectIdentifier(0); } -- (void)tearDown { - // Put teardown code here. This method is called after the invocation of each test method in the class. -} +#pragma mark - Identifiers - (void)testIdentifiers { ZENIdentifier nextIdentifier = ZENGetNextObjectIdentifier(); - XCTAssert(nextIdentifier == 1); - + XCTAssertEqual(nextIdentifier, 1); + nextIdentifier = ZENGetNextObjectIdentifier(); - XCTAssert(nextIdentifier == 2); - - ZENTestIdentifiable* obj = [ZENTestIdentifiable new]; - ZENIdentifier objID = obj.identifier; - XCTAssert(objID == 3); - + XCTAssertEqual(nextIdentifier, 2); + + ZENTestIdentifiable *obj = [ZENTestIdentifiable new]; + XCTAssertEqual(obj.identifier, 3); + nextIdentifier = ZENGetNextObjectIdentifier(); - XCTAssert(nextIdentifier == 4); + XCTAssertEqual(nextIdentifier, 4); } +#pragma mark - Cache / Retrieve + - (void)testNonExistingObject { ZENSetLargestObjectIdentifier(2); ZENIdentifier identifier = ZENGetNextObjectIdentifier(); - XCTAssert(identifier == 3); - - ZENTestIdentifiable* obj = [ZENTestIdentifiable testIdentifiableWithIdentifier:identifier]; + XCTAssertEqual(identifier, 3); + + ZENTestIdentifiable *obj = [ZENTestIdentifiable testIdentifiableWithIdentifier:identifier]; [self.cache cacheObject:obj]; - + id cachedObject = [self.cache cachedObject:1]; - XCTAssert(cachedObject == nil); + XCTAssertNil(cachedObject); } - (void)testExistingObject { ZENSetLargestObjectIdentifier(201); ZENIdentifier identifier = ZENGetNextObjectIdentifier(); - XCTAssert(identifier == 202); - - ZENTestIdentifiable* objOne = [ZENTestIdentifiable testIdentifiableWithIdentifier:identifier]; + XCTAssertEqual(identifier, 202); + + ZENTestIdentifiable *objOne = [ZENTestIdentifiable testIdentifiableWithIdentifier:identifier]; [self.cache cacheObject:objOne]; - - ZENIdentifier objOneID = objOne.identifier; - ZENTestIdentifiable* objTwo = [self.cache cachedObject:objOneID]; - XCTAssert(objOne == objTwo); - XCTAssert(objOneID == objTwo.identifier); + ZENTestIdentifiable *objTwo = [self.cache cachedObject:objOne.identifier]; + XCTAssertEqual(objOne, objTwo); + XCTAssertEqual(objOne.identifier, objTwo.identifier); +} + +#pragma mark - Remove + +- (void)testRemoveObject { + ZENIdentifier id1 = ZENGetNextObjectIdentifier(); + ZENTestIdentifiable *obj = [ZENTestIdentifiable testIdentifiableWithIdentifier:id1]; + [self.cache cacheObject:obj]; + + XCTAssertNotNil([self.cache cachedObject:id1]); + + [self.cache removeObject:id1]; + + // removeObject uses dispatch_barrier_async, give it a moment to complete + NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:1.0]; + while ([self.cache cachedObject:id1] != nil && [timeout timeIntervalSinceNow] > 0) { + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; + } + XCTAssertNil([self.cache cachedObject:id1], @"Object should be removed from cache"); +} + +#pragma mark - All Cached Objects + +- (void)testAllCachedObjects { + ZENObjectCache *strongCache = [ZENObjectCache strongObjectCache]; + + ZENTestIdentifiable *a = [ZENTestIdentifiable testIdentifiableWithIdentifier:ZENGetNextObjectIdentifier()]; + ZENTestIdentifiable *b = [ZENTestIdentifiable testIdentifiableWithIdentifier:ZENGetNextObjectIdentifier()]; + ZENTestIdentifiable *c = [ZENTestIdentifiable testIdentifiableWithIdentifier:ZENGetNextObjectIdentifier()]; + + [strongCache cacheObject:a]; + [strongCache cacheObject:b]; + [strongCache cacheObject:c]; + + // cacheObject uses dispatch_barrier_async, allow it to settle + NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:1.0]; + while ([strongCache allCachedObjects].count < 3 && [timeout timeIntervalSinceNow] > 0) { + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; + } + + NSArray *all = [strongCache allCachedObjects]; + XCTAssertEqual(all.count, 3u); +} + +#pragma mark - Remove All + +- (void)testRemoveAllObjects { + ZENObjectCache *strongCache = [ZENObjectCache strongObjectCache]; + + [strongCache cacheObject:[ZENTestIdentifiable testIdentifiableWithIdentifier:ZENGetNextObjectIdentifier()]]; + [strongCache cacheObject:[ZENTestIdentifiable testIdentifiableWithIdentifier:ZENGetNextObjectIdentifier()]]; + + [strongCache removeAllObjects]; + + // Both removeAllObjects and cacheObject use dispatch_barrier_async + NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:1.0]; + while ([strongCache allCachedObjects].count > 0 && [timeout timeIntervalSinceNow] > 0) { + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; + } + + XCTAssertEqual([strongCache allCachedObjects].count, 0u, @"Cache should be empty after removeAll"); +} + +#pragma mark - Strong vs Weak + +- (void)testStrongCacheRetainsObject { + ZENObjectCache *strongCache = [ZENObjectCache strongObjectCache]; + ZENIdentifier id1 = ZENGetNextObjectIdentifier(); + + @autoreleasepool { + ZENTestIdentifiable *obj = [ZENTestIdentifiable testIdentifiableWithIdentifier:id1]; + [strongCache cacheObject:obj]; + } + + // cacheObject is barrier-async; wait for it to land + NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:1.0]; + while ([strongCache cachedObject:id1] == nil && [timeout timeIntervalSinceNow] > 0) { + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; + } + + XCTAssertNotNil([strongCache cachedObject:id1], + @"Strong cache should keep the object alive after external refs are released"); +} + +- (void)testWeakCacheReleasesObject { + ZENObjectCache *weakCache = [ZENObjectCache weakObjectCache]; + ZENIdentifier id1 = ZENGetNextObjectIdentifier(); + + @autoreleasepool { + ZENTestIdentifiable *obj = [ZENTestIdentifiable testIdentifiableWithIdentifier:id1]; + [weakCache cacheObject:obj]; + + // Barrier-async write; wait for it to land before leaving the pool + NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:1.0]; + while ([weakCache cachedObject:id1] == nil && [timeout timeIntervalSinceNow] > 0) { + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; + } + XCTAssertNotNil([weakCache cachedObject:id1], @"Object should be in cache while alive"); + } + + // After the autorelease pool drains, the weak reference should clear + XCTAssertNil([weakCache cachedObject:id1], + @"Weak cache should release the object when no external strong refs remain"); } @end diff --git a/CoreZenTests/QueueTests.m b/CoreZenTests/QueueTests.m new file mode 100644 index 0000000..6788085 --- /dev/null +++ b/CoreZenTests/QueueTests.m @@ -0,0 +1,169 @@ +// +// QueueTests.m +// CoreZenTests +// + +#import + +@import CoreZen; + +@interface QueueTests : XCTestCase + +@end + +@implementation QueueTests + +#pragma mark - Token + +- (void)testTokenStartsDeactivated { + ZENWorkQueueToken *token = [ZENWorkQueueToken new]; + XCTAssertFalse(token.activated); + XCTAssertFalse(token.canceled); + XCTAssertFalse(token.terminated); +} + +- (void)testTokenActivation { + ZENWorkQueueToken *token = [ZENWorkQueueToken new]; + BOOL didActivate = [token activate]; + XCTAssertTrue(didActivate, @"First activation should succeed"); + XCTAssertTrue(token.activated); +} + +- (void)testTokenDoubleActivationReturnsFalse { + ZENWorkQueueToken *token = [ZENWorkQueueToken new]; + [token activate]; + BOOL secondActivate = [token activate]; + XCTAssertFalse(secondActivate, @"Second activation should fail"); + XCTAssertTrue(token.activated, @"Token should remain activated"); +} + +- (void)testTokenCancelSynonymForActivate { + ZENWorkQueueToken *token = [ZENWorkQueueToken new]; + BOOL didCancel = [token cancel]; + XCTAssertTrue(didCancel); + XCTAssertTrue(token.canceled); + XCTAssertTrue(token.activated); +} + +- (void)testTokenTerminateSynonymForActivate { + ZENWorkQueueToken *token = [ZENWorkQueueToken new]; + BOOL didTerminate = [token terminate]; + XCTAssertTrue(didTerminate); + XCTAssertTrue(token.terminated); + XCTAssertTrue(token.activated); +} + +#pragma mark - WorkQueue Creation + +- (void)testCreateWorkQueue { + ZENWorkQueue *queue = [ZENWorkQueue workQueue:@"test.queue"]; + XCTAssertNotNil(queue); + XCTAssertEqualObjects(queue.label, @"test.queue"); + [queue terminate:^{}]; +} + +- (void)testCreateWorkQueueWithQoS { + ZENWorkQueue *queue = [ZENWorkQueue workQueue:@"test.qos" qos:QOS_CLASS_UTILITY]; + XCTAssertNotNil(queue); + XCTAssertEqualObjects(queue.label, @"test.qos"); + [queue terminate:^{}]; +} + +#pragma mark - Sync Execution + +- (void)testSyncBlockIsCalled { + ZENWorkQueue *queue = [ZENWorkQueue workQueue:@"test.sync"]; + __block BOOL called = NO; + + BOOL result = [queue sync:^{ + called = YES; + }]; + + XCTAssertTrue(result, @"sync should return YES when block is called"); + XCTAssertTrue(called, @"Block should have been called synchronously"); + [queue terminate:^{}]; +} + +- (void)testSyncAfterTerminateReturnsFalse { + ZENWorkQueue *queue = [ZENWorkQueue workQueue:@"test.sync.term"]; + [queue terminate:^{}]; + + BOOL result = [queue sync:^{}]; + XCTAssertFalse(result, @"sync after terminate should return NO"); +} + +#pragma mark - Async Execution + +- (void)testAsyncBlockIsCalled { + ZENWorkQueue *queue = [ZENWorkQueue workQueue:@"test.async"]; + XCTestExpectation *expectation = [self expectationWithDescription:@"async block called"]; + + ZENWorkQueueToken *token = [queue async:^(ZENWorkQueueToken *canceled) { + XCTAssertFalse(canceled.canceled, @"Should not be canceled"); + [expectation fulfill]; + }]; + + XCTAssertNotNil(token, @"Should return a cancel token"); + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + [queue terminate:^{}]; +} + +- (void)testAsyncAfterTerminateReturnsNil { + ZENWorkQueue *queue = [ZENWorkQueue workQueue:@"test.async.term"]; + [queue terminate:^{}]; + + ZENWorkQueueToken *token = [queue async:^(ZENWorkQueueToken *canceled) { + XCTFail(@"Block should not be called after terminate"); + }]; + XCTAssertNil(token, @"async after terminate should return nil"); +} + +#pragma mark - Cancellation + +- (void)testCancelTokenReflectsInBlock { + ZENWorkQueue *queue = [ZENWorkQueue workQueue:@"test.cancel"]; + XCTestExpectation *expectation = [self expectationWithDescription:@"cancel check"]; + + // Queue a slow first job to give us time to cancel the second + [queue async:^(ZENWorkQueueToken *canceled) { + [NSThread sleepForTimeInterval:0.1]; + }]; + + ZENWorkQueueToken *cancelToken = [queue async:^(ZENWorkQueueToken *canceled) { + // The composite token should see our cancellation + XCTAssertTrue(canceled.canceled, @"Should see cancellation"); + [expectation fulfill]; + }]; + + [cancelToken cancel]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + [queue terminate:^{}]; +} + +#pragma mark - Termination + +- (void)testTerminateCallsBlock { + ZENWorkQueue *queue = [ZENWorkQueue workQueue:@"test.terminate"]; + __block BOOL terminateCalled = NO; + + [queue terminate:^{ + terminateCalled = YES; + }]; + + XCTAssertTrue(terminateCalled, @"Terminate block should be called synchronously"); +} + +- (void)testSyncPreservesOrder { + ZENWorkQueue *queue = [ZENWorkQueue workQueue:@"test.order"]; + NSMutableArray *order = [NSMutableArray new]; + + [queue sync:^{ [order addObject:@1]; }]; + [queue sync:^{ [order addObject:@2]; }]; + [queue sync:^{ [order addObject:@3]; }]; + + NSArray *expected = @[@1, @2, @3]; + XCTAssertEqualObjects(order, expected, @"Sync blocks should execute in order"); + [queue terminate:^{}]; +} + +@end