Skip to content

Commit 62e6a40

Browse files
ampagenttmm
andcommitted
test(swift): the whole client against zero-cache end to end
Syncs a named query, runs mutations optimistically until the API server confirms them, sees them from a second client, and relaunches on the same SQLite store. The test schema is generated for the E2E target too. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
1 parent a717844 commit 62e6a40

3 files changed

Lines changed: 624 additions & 0 deletions

File tree

‎packages/zero-swift-cli/src/codegen.test.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ const fixtures: {name: string; config: Config}[] = [
3131
access: 'internal',
3232
},
3333
},
34+
{
35+
name: 'zql test schema for the end-to-end tests',
36+
config: {
37+
schema: 'packages/zql/src/query/test/test-schemas.ts',
38+
output: 'swift/Tests/E2ETests/Generated/TestSchema.swift',
39+
schemaName: 'test',
40+
zql: 'testZQL',
41+
typePrefix: 'Test',
42+
access: 'internal',
43+
},
44+
},
3445
{
3546
name: 'codegen fixture schema',
3647
config: {
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import Foundation
2+
import Testing
3+
import Zero
4+
5+
/// The app of the end-to-end server (`packages/zero-conformance/src/e2e/app.ts`),
6+
/// defined in Swift as an app would.
7+
enum E2EApp {
8+
static let open = defineQuery("issues.open") { () in
9+
testZQL.issue.where(\.closed, false).related(\.comments).related(\.owner)
10+
.orderBy(\.createdAt, .asc)
11+
}
12+
static let byID = defineQuery("issues.byID") { (id: String) in
13+
testZQL.issue.where(\.id, id).related(\.comments).one()
14+
}
15+
16+
struct CreateIssue: Codable, Sendable {
17+
var id: String
18+
var title: String
19+
var ownerId: String?
20+
}
21+
22+
static let create = defineMutator("issue.create") { (tx, args: CreateIssue) in
23+
try tx.mutate.insert(
24+
TestIssue(
25+
id: args.id, title: args.title, description: "", closed: false, ownerId: args.ownerId,
26+
createdAt: 100))
27+
}
28+
static let close = defineMutator("issue.close") { (tx, id: String) in
29+
try tx.mutate.issue.update(["id": .string(id), "closed": true])
30+
}
31+
}
32+
33+
/// The whole client against zero-cache, the API server, and Postgres: sync,
34+
/// optimistic mutations the server confirms, and a relaunch on the same
35+
/// store.
36+
@Suite(.enabled(if: E2E.url != nil), .timeLimit(.minutes(2)))
37+
@MainActor
38+
struct ClientE2ETests {
39+
let directory = NSTemporaryDirectory() + "zero-e2e-" + UUID().uuidString
40+
41+
func makeZero(storageKey: String) throws -> Zero<Void> {
42+
let origin = E2E.url!.replacingOccurrences(of: "ws://", with: "http://")
43+
let options = ZeroOptions(
44+
cacheURL: origin, storageKey: storageKey, storage: .sqlite(directory: directory))
45+
return try Zero(
46+
schema: .test, mutators: [E2EApp.create, E2EApp.close], options: options,
47+
socketFactory: TransportSocketFactory(NIOTransport()))
48+
}
49+
50+
/// Waits until `condition` holds.
51+
func eventually(
52+
_ what: String, timeout: Duration = .seconds(30), _ condition: () -> Bool
53+
) async throws {
54+
let deadline = ContinuousClock.now + timeout
55+
while !condition() {
56+
guard ContinuousClock.now < deadline else {
57+
Issue.record("Timed out waiting for \(what)")
58+
throw CancellationError()
59+
}
60+
try await Task.sleep(for: .milliseconds(20))
61+
}
62+
}
63+
64+
@Test func syncsMutatesAndRelaunches() async throws {
65+
defer { try? FileManager.default.removeItem(atPath: directory) }
66+
let key = UUID().uuidString
67+
let zero = try makeZero(storageKey: key)
68+
let open = try zero.liveQuery(E2EApp.open())
69+
try await eventually("the open issues") { open.resultType == .complete }
70+
#expect(Set(["i1", "i3", "i4"]).isSubset(of: open.data.map(\.id)))
71+
let i1 = try #require(open.data.first { $0.id == "i1" })
72+
#expect(i1.comments?.map(\.id) == ["c1", "c2"])
73+
#expect(i1.owner?.name == "Alice")
74+
#expect(open.data.first { $0.id == "i3" }?.title == "Emoji 😀 in titles")
75+
76+
// Optimistic at once, then confirmed by the server.
77+
let id = "swift-\(UUID().uuidString)"
78+
let created = zero.mutate(E2EApp.create(.init(id: id, title: "From Swift", ownerId: "u2")))
79+
#expect(open.data.contains { $0.id == id })
80+
#expect(try await created.client.value == .success(data: nil))
81+
#expect(try await created.server.value == .success(data: nil))
82+
let confirmed = try await zero.run(E2EApp.byID(id), type: .complete)
83+
#expect(confirmed?.title == "From Swift")
84+
85+
// Another client sees it through the server.
86+
let other = try makeZero(storageKey: UUID().uuidString)
87+
let seen = try await other.run(E2EApp.byID(id), type: .complete)
88+
#expect(seen?.ownerId == "u2")
89+
90+
let closed = zero.mutate(E2EApp.close(id))
91+
#expect(try await closed.server.value == .success(data: nil))
92+
#expect(!open.data.contains { $0.id == id })
93+
let liveOther = try other.liveQuery(E2EApp.byID(id))
94+
try await eventually("the close to sync") { liveOther.data?.closed == true }
95+
other.close()
96+
97+
// A relaunch reads the rows from its store before the server answers.
98+
zero.close()
99+
let relaunched = try makeZero(storageKey: key)
100+
defer { relaunched.close() }
101+
#expect(relaunched.clientGroupID == zero.clientGroupID)
102+
let cached = try await relaunched.run(E2EApp.open(), type: .cached)
103+
#expect(Set(["i1", "i3", "i4"]).isSubset(of: cached.map(\.id)))
104+
#expect(!cached.contains { $0.id == id })
105+
}
106+
}

0 commit comments

Comments
 (0)