Skip to content

Commit 282ed06

Browse files
wip: Add unit tests
1 parent baafffe commit 282ed06

File tree

6 files changed

+313
-2
lines changed

6 files changed

+313
-2
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Scheme
3+
LastUpgradeVersion = "1630"
4+
version = "1.7">
5+
<BuildAction
6+
parallelizeBuildables = "YES"
7+
buildImplicitDependencies = "YES"
8+
buildArchitectures = "Automatic">
9+
<BuildActionEntries>
10+
<BuildActionEntry
11+
buildForTesting = "YES"
12+
buildForRunning = "YES"
13+
buildForProfiling = "YES"
14+
buildForArchiving = "YES"
15+
buildForAnalyzing = "YES">
16+
<BuildableReference
17+
BuildableIdentifier = "primary"
18+
BlueprintIdentifier = "PowerSync"
19+
BuildableName = "PowerSync"
20+
BlueprintName = "PowerSync"
21+
ReferencedContainer = "container:">
22+
</BuildableReference>
23+
</BuildActionEntry>
24+
</BuildActionEntries>
25+
</BuildAction>
26+
<TestAction
27+
buildConfiguration = "Debug"
28+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
29+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
30+
shouldUseLaunchSchemeArgsEnv = "YES"
31+
shouldAutocreateTestPlan = "YES">
32+
<Testables>
33+
<TestableReference
34+
skipped = "NO">
35+
<BuildableReference
36+
BuildableIdentifier = "primary"
37+
BlueprintIdentifier = "PowerSyncTests"
38+
BuildableName = "PowerSyncTests"
39+
BlueprintName = "PowerSyncTests"
40+
ReferencedContainer = "container:">
41+
</BuildableReference>
42+
</TestableReference>
43+
</Testables>
44+
</TestAction>
45+
<LaunchAction
46+
buildConfiguration = "Debug"
47+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
48+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
49+
launchStyle = "0"
50+
useCustomWorkingDirectory = "NO"
51+
ignoresPersistentStateOnLaunch = "NO"
52+
debugDocumentVersioning = "YES"
53+
debugServiceExtension = "internal"
54+
allowLocationSimulation = "YES">
55+
</LaunchAction>
56+
<ProfileAction
57+
buildConfiguration = "Release"
58+
shouldUseLaunchSchemeArgsEnv = "YES"
59+
savedToolIdentifier = ""
60+
useCustomWorkingDirectory = "NO"
61+
debugDocumentVersioning = "YES">
62+
<MacroExpansion>
63+
<BuildableReference
64+
BuildableIdentifier = "primary"
65+
BlueprintIdentifier = "PowerSync"
66+
BuildableName = "PowerSync"
67+
BlueprintName = "PowerSync"
68+
ReferencedContainer = "container:">
69+
</BuildableReference>
70+
</MacroExpansion>
71+
</ProfileAction>
72+
<AnalyzeAction
73+
buildConfiguration = "Debug">
74+
</AnalyzeAction>
75+
<ArchiveAction
76+
buildConfiguration = "Release"
77+
revealArchiveInOrganizer = "YES">
78+
</ArchiveAction>
79+
</Scheme>

Package.resolved

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ let package = Package(
1616
targets: ["PowerSync"]),
1717
],
1818
dependencies: [
19-
.package(path: "/Users/stevenontong/Documents/platform_code/powersync/powersync-kotlin"),
20-
// .package(url: "https://github.com/powersync-ja/powersync-kotlin.git", exact: "1.0.0-BETA28.0"),
19+
.package(url: "https://github.com/powersync-ja/powersync-kotlin.git", exact: "1.0.0-BETA28.0"),
2120
.package(url: "https://github.com/powersync-ja/powersync-sqlite-core-swift.git", "0.3.12"..<"0.4.0")
2221
],
2322
targets: [
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
func createAttachmentsTable(name: String) -> Table {
2+
return Table(name: name, columns: [
3+
.integer("timestamp"),
4+
.integer("state"),
5+
.text("filename"),
6+
.integer("has_synced"),
7+
.text("local_uri"),
8+
.text("media_type"),
9+
.integer("size")
10+
])
11+
}
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
2+
@testable import PowerSync
3+
import XCTest
4+
5+
final class AttachmentTests: XCTestCase {
6+
private var database: PowerSyncDatabaseProtocol!
7+
private var schema: Schema!
8+
9+
override func setUp() async throws {
10+
try await super.setUp()
11+
schema = Schema(tables: [
12+
Table(name: "users", columns: [
13+
.text("name"),
14+
.text("email"),
15+
.text("photo_id")
16+
]),
17+
createAttachmentsTable(name: "attachments")
18+
])
19+
20+
database = PowerSyncDatabase(
21+
schema: schema,
22+
dbFilename: ":memory:"
23+
)
24+
try await database.disconnectAndClear()
25+
}
26+
27+
override func tearDown() async throws {
28+
try await database.disconnectAndClear()
29+
database = nil
30+
try await super.tearDown()
31+
}
32+
33+
func testAttachmentDownload() async throws {
34+
let queue = AttachmentQueue(
35+
db: database,
36+
remoteStorage: {
37+
struct MockRemoteStorage: RemoteStorageAdapter {
38+
func uploadFile(
39+
fileData: Data,
40+
attachment: Attachment
41+
) async throws {}
42+
43+
/**
44+
* Download a file from remote storage
45+
*/
46+
func downloadFile(attachment: Attachment) async throws -> Data {
47+
return Data([1,2,3])
48+
}
49+
50+
/**
51+
* Delete a file from remote storage
52+
*/
53+
func deleteFile(attachment: Attachment) async throws {}
54+
55+
}
56+
57+
return MockRemoteStorage()
58+
}(),
59+
attachmentDirectory: NSTemporaryDirectory(),
60+
watchedAttachments: try database.watch(options: WatchOptions(
61+
sql: "SELECT photo_id FROM users WHERE photo_id IS NOT NULL",
62+
mapper: { cursor in WatchedAttachmentItem(
63+
id: try cursor.getString(name: "photo_id"),
64+
fileExtension: "jpg"
65+
)}
66+
))
67+
)
68+
69+
try await queue.startSync()
70+
71+
// Create a user which has a photo_id associated.
72+
// This will be treated as a download since no attachment record was created.
73+
// saveFile creates the attachment record before the updates are made.
74+
_ = try await database.execute(
75+
sql: "INSERT INTO users (id, name, email, photo_id) VALUES (uuid(), 'steven', '[email protected]', uuid())",
76+
parameters: []
77+
)
78+
79+
var attachmentsWatch = try database.watch(
80+
options: WatchOptions(
81+
sql: "SELECT * FROM attachments",
82+
mapper: {cursor in try Attachment.fromCursor(cursor)}
83+
)).makeAsyncIterator()
84+
85+
var attachmentRecord = try await waitForMatch(
86+
iterator: attachmentsWatch,
87+
where: {results in results.first?.state == AttachmentState.synced.rawValue},
88+
timeout: 5
89+
).first
90+
91+
// The file should exist
92+
let localData = try await queue.localStorage.readFile(filePath: attachmentRecord!.localUri!)
93+
XCTAssertEqual(localData.count, 3)
94+
95+
try await queue.clearQueue()
96+
try await queue.close()
97+
}
98+
99+
func testAttachmentUpload() async throws {
100+
101+
class MockRemoteStorage: RemoteStorageAdapter {
102+
public var uploadCalled = false
103+
104+
func uploadFile(
105+
fileData: Data,
106+
attachment: Attachment
107+
) async throws {
108+
self.uploadCalled = true
109+
}
110+
111+
/**
112+
* Download a file from remote storage
113+
*/
114+
func downloadFile(attachment: Attachment) async throws -> Data {
115+
return Data([1,2,3])
116+
}
117+
118+
/**
119+
* Delete a file from remote storage
120+
*/
121+
func deleteFile(attachment: Attachment) async throws {}
122+
123+
}
124+
125+
126+
127+
let mockedRemote = MockRemoteStorage()
128+
129+
let queue = AttachmentQueue(
130+
db: database,
131+
remoteStorage: mockedRemote,
132+
attachmentDirectory: NSTemporaryDirectory(),
133+
watchedAttachments: try database.watch(options: WatchOptions(
134+
sql: "SELECT photo_id FROM users WHERE photo_id IS NOT NULL",
135+
mapper: { cursor in WatchedAttachmentItem(
136+
id: try cursor.getString(name: "photo_id"),
137+
fileExtension: "jpg"
138+
)}
139+
))
140+
)
141+
142+
try await queue.startSync()
143+
144+
let attachmentsWatch = try database.watch(
145+
options: WatchOptions(
146+
sql: "SELECT * FROM attachments",
147+
mapper: {cursor in try Attachment.fromCursor(cursor)}
148+
)).makeAsyncIterator()
149+
150+
_ = try await queue.saveFile(
151+
data: Data([3,4,5]),
152+
mediaType: "image/jpg",
153+
fileExtension: "jpg") {tx, attachment in
154+
_ = try tx.execute(
155+
sql: "INSERT INTO users (id, name, email, photo_id) VALUES (uuid(), 'john', '[email protected]', ?)",
156+
parameters: [attachment.id]
157+
)
158+
}
159+
160+
_ = try await waitForMatch(
161+
iterator: attachmentsWatch,
162+
where: {results in results.first?.state == AttachmentState.synced.rawValue},
163+
timeout: 5
164+
).first
165+
166+
// Upload should have been called
167+
XCTAssertTrue(mockedRemote.uploadCalled)
168+
169+
try await queue.clearQueue()
170+
try await queue.close()
171+
}
172+
}
173+
174+
175+
enum WaitForMatchError: Error {
176+
case timeout
177+
}
178+
179+
func waitForMatch<T, E: Error>(
180+
iterator: AsyncThrowingStream<T, E>.Iterator,
181+
where predicate: @escaping (T) -> Bool,
182+
timeout: TimeInterval
183+
) async throws -> T {
184+
var localIterator = iterator
185+
let timeoutNanoseconds = UInt64(timeout * 1_000_000_000)
186+
187+
return try await withThrowingTaskGroup(of: T.self) { group in
188+
// Task to wait for a matching value
189+
group.addTask {
190+
while let value = try await localIterator.next() {
191+
if predicate(value) {
192+
return value
193+
}
194+
}
195+
throw WaitForMatchError.timeout // stream ended before match
196+
}
197+
198+
// Task to enforce timeout
199+
group.addTask {
200+
try await Task.sleep(nanoseconds: timeoutNanoseconds)
201+
throw WaitForMatchError.timeout
202+
}
203+
204+
// First one to succeed or fail
205+
let result = try await group.next()
206+
group.cancelAll()
207+
return result!
208+
}
209+
}

Tests/PowerSyncTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase {
1111
Table(name: "users", columns: [
1212
.text("name"),
1313
.text("email"),
14+
.text("photo_id")
1415
]),
16+
createAttachmentsTable(name: "attachments")
1517
])
1618

1719
database = KotlinPowerSyncDatabaseImpl(
@@ -63,6 +65,8 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase {
6365
XCTAssertEqual(user.1, "Test User")
6466
XCTAssertEqual(user.2, "[email protected]")
6567
}
68+
69+
6670

6771
func testGetError() async throws {
6872
do {

0 commit comments

Comments
 (0)