Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CanopyResultRecord #22

Merged
merged 57 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
a8f3b28
New type stubs
Jul 25, 2024
fcef8ad
CanopyRecordValueProtocol
jaanus Jul 26, 2024
19b26a5
CanopyResultRecord with tests
jaanus Jul 26, 2024
038dd12
MockValueStore Codable scaffolding
jaanus Jul 27, 2024
c205bc9
Encoding NSNumber (even though it’s never hit)
Jul 28, 2024
3ea4cda
Force NSType
Jul 28, 2024
f04f250
MockValueStore Array support
Jul 29, 2024
b3e53d6
Int and UInt
Jul 29, 2024
55302d6
NSString coding
Jul 29, 2024
811e14f
date and nsdate encoding
Jul 29, 2024
8ba83f3
float, data, nsdata coding
Jul 29, 2024
a9791c4
Doc and test about NSArray coding
Jul 29, 2024
3c75c62
MockValueStore is done
Jul 29, 2024
5e90713
Formatting
Jul 29, 2024
abdbbcb
MockCanopyResultRecord codable
Jul 29, 2024
43fe9c1
CanopyResultRecord Codable
Jul 29, 2024
efa6119
MockValueStore moved inside MockCanopyResultRecord
Jul 30, 2024
382a0b4
Start with ReplayingMockContainer and Database
Jul 30, 2024
2e4f31d
Moved more types around
jaanus Jul 30, 2024
b9a95a1
Remove unneeded print
jaanus Jul 30, 2024
5f95181
Removed unneeded prints
jaanus Jul 30, 2024
864e22c
ReplayingMockContainer done
jaanus Jul 31, 2024
f9b1f88
Fewer warnings for error casting
jaanus Jul 31, 2024
13b2737
Renamed
jaanus Jul 31, 2024
c8830d7
Better name for MockValueStoreTests
jaanus Jul 31, 2024
1414ae8
Converted Canopy API to CanopyResultRecord
jaanus Jul 31, 2024
569fd3b
ReplayingMockDatabase queryRecords
jaanus Jul 31, 2024
1372340
ModifyRecordsResult Codable
jaanus Jul 31, 2024
b58b27b
Modify and delete records results
jaanus Jul 31, 2024
46670c5
FetchRecordsResult Codable
jaanus Jul 31, 2024
d283224
ReplayingMockDatabase WIP
jaanus Jul 31, 2024
f51d812
ModifyZonesResult Codable
jaanus Aug 1, 2024
c6ec21e
More results Codable and tests
jaanus Aug 1, 2024
8842428
MockDatabase done, todo tests
jaanus Aug 1, 2024
71cd354
All ReplayingMockDatabase tests
jaanus Aug 1, 2024
4502542
Fixed test failure in Sequoia with Xcode 16 beta 4
Aug 6, 2024
d9c2175
Moved types around between modules
Aug 6, 2024
702ae27
Static blank result for fetch database changes
Aug 6, 2024
11fb42c
Canopy type conformance in extension
Aug 6, 2024
45a947a
Removed MockCKRecord
jaanus Aug 6, 2024
12ca9a0
Mock container is more resilient now
jaanus Aug 6, 2024
c432bd3
Adjustments
jaanus Aug 7, 2024
ab5c345
More MOckCanopyResultRecord tests
jaanus Aug 8, 2024
87419c8
More CanopyResultRecord tests
jaanus Aug 8, 2024
771960f
sleepBeforeEachOperation
jaanus Aug 10, 2024
ced84f7
Default values for some parameters
jaanus Aug 10, 2024
db4c537
SleepIfNeeded unit test
jaanus Aug 10, 2024
5182ee1
Maybe Github CI is happier with this?
jaanus Aug 10, 2024
10864cf
Maybe this will satisfy Github CI
jaanus Aug 10, 2024
585c505
Shorter code, produces the same crash
jaanus Aug 10, 2024
316e54a
Trying with a different signature for subscript
jaanus Aug 12, 2024
54873b1
How about matching signatures in the other direction
jaanus Aug 12, 2024
f5d2e45
Merge branch 'main' into canopy-result-record-type
Aug 15, 2024
e554648
Updated encryptedValues API name
jaanus Aug 17, 2024
11bd774
Warning about documentation site
jaanus Aug 20, 2024
d74c5bb
Some documentation comments
jaanus Aug 20, 2024
7e30615
Updated version in readme
jaanus Aug 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions .swiftpm/xcode/xcshareddata/xcschemes/Canopy-Package.xcscheme
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Canopy"
BuildableName = "Canopy"
BlueprintName = "Canopy"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CanopyTestTools"
BuildableName = "CanopyTestTools"
BlueprintName = "CanopyTestTools"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CanopyTests"
BuildableName = "CanopyTests"
BlueprintName = "CanopyTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Canopy"
BuildableName = "Canopy"
BlueprintName = "Canopy"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
2 changes: 1 addition & 1 deletion .swiftpm/xcode/xcshareddata/xcschemes/Canopy.xcscheme
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
LastUpgradeVersion = "1600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
LastUpgradeVersion = "1600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ let package = Package(
.testTarget(
name: "CanopyTests",
dependencies: ["Canopy", "CanopyTestTools"],
path: "Targets/Canopy/Tests"
path: "Targets/Canopy/Tests",
resources: [
.process("Fixtures")
]
),
.target(
name: "CanopyTypes",
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ If you use SPM `Package.swift`, add this:
dependencies: [
.package(
url: "https://github.com/Tact/Canopy",
from: "0.2.0"
from: "0.5.0"
)
]
```
Expand Down
6 changes: 3 additions & 3 deletions Targets/Canopy/Sources/CKDatabaseAPI/CKDatabaseAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ actor CKDatabaseAPI: CKDatabaseAPIType {
in zoneID: CKRecordZone.ID?,
resultsLimit: Int?,
qos: QualityOfService
) async -> Result<[CKRecord], CKRecordError> {
) async -> Result<[CanopyResultRecord], CKRecordError> {
await QueryRecords.with(
query,
recordZoneID: zoneID,
Expand Down Expand Up @@ -160,7 +160,7 @@ actor CKDatabaseAPI: CKDatabaseAPIType {
continuation.resume(
returning: .success(
.init(
foundRecords: foundRecords,
foundRecords: foundRecords.map(\.canopyResultRecord),
notFoundRecordIDs: notFoundRecordIDs
)
)
Expand Down Expand Up @@ -567,7 +567,7 @@ actor CKDatabaseAPI: CKDatabaseAPIType {
continuation.resume(
returning: .success(
.init(
records: records,
records: records.map { $0.canopyResultRecord },
deletedRecords: deleted
)
)
Expand Down
2 changes: 2 additions & 0 deletions Targets/Canopy/Sources/Canopy.docc/Canopy.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Write better, testable CloudKit apps.

_⚠️ This documentation site is currently half-broken, and does not include documentation for several Canopy types. Conceptual documentation works fine. Canopy types and code are spread across several Swift Package Manager modules, and DocC does not easily support this scenario out of the box, for generating a documentation site. [Work is in progress](https://forums.swift.org/t/are-there-updates-on-using-swift-docc-with-multiple-targets/73072) to address this._

Canopy helps you write better, more testable CloudKit apps. It isolates the CloudKit dependency so you can write fast and reliable tests for your CloudKit-related features, and implements standard CloudKit-related behaviors.

Canopy is built as part of [Tact](https://justtact.com). The Canopy source code and installation instructions (including the source for this site) are available on [GitHub](https://github.com/Tact/Canopy).
Expand Down
3 changes: 3 additions & 0 deletions Targets/Canopy/Sources/Canopy/Canopy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import CanopyTypes
import CloudKit
import Foundation

// Re-export the types, so `import Canopy` also imports the types.
@_exported import CanopyTypes

/// Main Canopy implementation.
///
/// You construct Canopy with injected CloudKit container and databases, token store, and settings provider.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,59 @@ import CloudKit
/// where you need to isolate the CloudKit dependency and provide a
/// deterministic view of CloudKit data with simulated mock data.
///
/// You initialize MockCanopy with instances of mock container and databases.
/// You initialize MockCanopy with instances of mock CKContainer and CKDatabases.
/// The Canopy API then receives API calls and plays back the responses to those
/// requests, without any interaction with real CloudKit.
///
/// You only need to inject the containers and databases that your tests actually use.
/// If you try to use a dependency that’s not been injected correctly, MockCanopy crashes
/// with an error message indicating that.
///
/// MockCanopyWithCKMocks is mostly appropriate to use as a testing tool for Canopy’s
/// own logic, or when you need to inject your own Canopy settings for various behaviors.
/// For using in your own tests, `MockCanopy` is more appropriate and simpler to use.
@available(iOS 16.4, macOS 13.3, *)
public struct MockCanopy: CanopyType {
private let mockPrivateDatabase: CKDatabaseType?
private let mockSharedDatabase: CKDatabaseType?
private let mockPublicDatabase: CKDatabaseType?
private let mockContainer: CKContainerType?
public struct MockCanopyWithCKMocks: CanopyType {
private let mockPrivateCKDatabase: CKDatabaseType?
private let mockSharedCKDatabase: CKDatabaseType?
private let mockPublicCKDatabase: CKDatabaseType?
private let mockCKContainer: CKContainerType?
private let settingsProvider: @Sendable () async -> CanopySettingsType

public init(
mockPrivateDatabase: CKDatabaseType? = nil,
mockSharedDatabase: CKDatabaseType? = nil,
mockPublicDatabase: CKDatabaseType? = nil,
mockContainer: CKContainerType? = nil,
mockPrivateCKDatabase: CKDatabaseType? = nil,
mockSharedCKDatabase: CKDatabaseType? = nil,
mockPublicCKDatabase: CKDatabaseType? = nil,
mockCKContainer: CKContainerType? = nil,
settingsProvider: @escaping @Sendable () async -> CanopySettingsType = { CanopySettings() }
) {
self.mockPublicDatabase = mockPublicDatabase
self.mockSharedDatabase = mockSharedDatabase
self.mockPrivateDatabase = mockPrivateDatabase
self.mockContainer = mockContainer
self.mockPublicCKDatabase = mockPublicCKDatabase
self.mockSharedCKDatabase = mockSharedCKDatabase
self.mockPrivateCKDatabase = mockPrivateCKDatabase
self.mockCKContainer = mockCKContainer
self.settingsProvider = settingsProvider
}

public func databaseAPI(usingDatabaseScope scope: CKDatabase.Scope) -> CKDatabaseAPIType {
switch scope {
case .public:
guard let db = mockPublicDatabase else { fatalError("Requested public database which wasn’t correctly injected") }
guard let db = mockPublicCKDatabase else { fatalError("Requested public database which wasn’t correctly injected") }
return CKDatabaseAPI(
database: db,
databaseScope: .public,
settingsProvider: settingsProvider,
tokenStore: TestTokenStore()
)
case .private:
guard let db = mockPrivateDatabase else { fatalError("Requested private database which wasn’t correctly injected") }
guard let db = mockPrivateCKDatabase else { fatalError("Requested private database which wasn’t correctly injected") }
return CKDatabaseAPI(
database: db,
databaseScope: .private,
settingsProvider: settingsProvider,
tokenStore: TestTokenStore()
)
case .shared:
guard let db = mockSharedDatabase else { fatalError("Requested shared database which wasn’t correctly injected") }
guard let db = mockSharedCKDatabase else { fatalError("Requested shared database which wasn’t correctly injected") }
return CKDatabaseAPI(
database: db,
databaseScope: .shared,
Expand All @@ -66,7 +70,7 @@ public struct MockCanopy: CanopyType {
}

public func containerAPI() -> CKContainerAPIType {
guard let container = mockContainer else { fatalError("Requested CKContainer which wasn’t correctly injected") }
guard let container = mockCKContainer else { fatalError("Requested CKContainer which wasn’t correctly injected") }
return CKContainerAPI(
container: container,
accountChangedSequence: .mock(elementsToProduce: 1)
Expand Down
2 changes: 0 additions & 2 deletions Targets/Canopy/Sources/Dependency/Canopy+Dependency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import Dependencies
@available(iOS 16.4, macOS 13.3, *)
private enum CanopyKey: DependencyKey, Sendable {
static let liveValue: CanopyType = Canopy()
static let testValue: CanopyType = MockCanopy()
static let previewValue: CanopyType = MockCanopy()
}

@available(iOS 16.4, macOS 13.3, *)
Expand Down
6 changes: 3 additions & 3 deletions Targets/Canopy/Sources/Features/ModifyRecords.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ struct ModifyRecords {
autoBatchToSmallerWhenLimitExceeded: Bool = true,
autoRetryForRetriableErrors: Bool = true
) async -> Result<ModifyRecordsResult, CKRecordError> {
var savedRecords: [CKRecord] = []
var savedRecords: [CanopyResultRecord] = []
var deletedRecordIDs: [CKRecord.ID] = []
var currentBatchSize = customBatchSize ?? CKBatchSize

Expand Down Expand Up @@ -68,7 +68,7 @@ struct ModifyRecords {

switch result {
case let .success(result):
savedRecords += result.savedRecords
savedRecords.append(contentsOf: result.savedRecords)
deletedRecordIDs += result.deletedRecordIDs
case let .failure(error):
if error == CKRecordError(from: CKError(CKError.Code.limitExceeded)), autoBatchToSmallerWhenLimitExceeded {
Expand Down Expand Up @@ -197,7 +197,7 @@ struct ModifyRecords {
continuation.resume(
returning: .success(
ModifyRecordsResult(
savedRecords: savedRecords,
savedRecords: savedRecords.map(\.canopyResultRecord),
deletedRecordIDs: deletedRecordIDs
)
)
Expand Down
8 changes: 5 additions & 3 deletions Targets/Canopy/Sources/Features/QueryRecords.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ struct QueryRecords {
desiredKeys: [CKRecord.FieldKey]? = nil,
resultsLimit: Int? = nil,
qualityOfService: QualityOfService = .default
) async -> Result<[CKRecord], CKRecordError> {
) async -> Result<[CanopyResultRecord], CKRecordError> {
var startingPoint = QueryOperationStartingPoint.query(query)
var records: [CKRecord] = []

Expand All @@ -50,14 +50,16 @@ struct QueryRecords {
case let .error(error):
return .failure(error)
case let .records(newRecords):
return .success(records + newRecords)
let ckRecords = records + newRecords
return .success(ckRecords.map(\.canopyResultRecord))
case let .recordsAndCursor(newRecords, cursor):
guard !Task.isCancelled else {
return .failure(.init(from: CKError(CKError.Code.operationCancelled)))
}
// If there was a results limit, just return the result even if there was a cursor
if resultsLimit != nil {
return .success(records + newRecords)
let ckRecords = records + newRecords
return .success(ckRecords.map(\.canopyResultRecord))
}

startingPoint = QueryOperationStartingPoint.cursor(cursor)
Expand Down
26 changes: 0 additions & 26 deletions Targets/Canopy/Sources/Results/FetchDatabaseChangesResult.swift

This file was deleted.

15 changes: 0 additions & 15 deletions Targets/Canopy/Sources/Results/FetchRecordsResult.swift

This file was deleted.

16 changes: 0 additions & 16 deletions Targets/Canopy/Sources/Results/ModifyRecordsResult.swift

This file was deleted.

11 changes: 0 additions & 11 deletions Targets/Canopy/Sources/Results/ModifySubscriptionsResult.swift

This file was deleted.

Loading