| 1 | import Foundation |
| 2 | import RastrilloApple |
| 3 | import XCTest |
| 4 | @testable import DocsKit |
| 5 | |
| 6 | final class DocumentSyncIntegrationTests: XCTestCase { |
| 7 | func testFailedMergeCannotPoisonEditorOrNextMerge() async throws { |
| 8 | let replica = CRDTReplica(guid: "merge-isolation") |
| 9 | _ = try await replica.open(update: Data()) |
| 10 | _ = try await replica.command(["op": .string("insert_element"), "at": .number(0), "type": .string("paragraph")]) |
| 11 | do { |
| 12 | _ = try await replica.mergeUpdates([Data([255]), Data([255])]) |
| 13 | XCTFail("Malformed updates must fail") |
| 14 | } catch { } |
| 15 | _ = try await replica.projection() |
| 16 | let (first, _) = try await replica.command(["op": .string("display_edit"), "at": .number(0), "length": .number(0), "text": .string("a")]) |
| 17 | let (second, projection) = try await replica.command(["op": .string("display_edit"), "at": .number(1), "length": .number(0), "text": .string("b")]) |
| 18 | let merged = try await replica.mergeUpdates([first, second]) |
| 19 | XCTAssertFalse(merged.isEmpty) |
| 20 | let after = try await replica.projection() |
| 21 | XCTAssertEqual(after.children.map(\.plainText), projection.children.map(\.plainText)) |
| 22 | await replica.close() |
| 23 | } |
| 24 | @MainActor func testTypingDoesNotAccumulateOneRoundTripPerKeystroke() async throws { |
| 25 | guard let origin = ProcessInfo.processInfo.environment["DOCS_SYNC_TEST_ORIGIN"] else { |
| 26 | throw XCTSkip("Run native/scripts/test-sync-latency.py for the delayed HTTP fixture") |
| 27 | } |
| 28 | let client = NativeHTTP(origin: URL(string: origin)!) |
| 29 | let model = DocsModel(client: client) |
| 30 | await model.open("fixture") |
| 31 | XCTAssertNotNil(model.projection) |
| 32 | model.didRender(model.projectionGeneration) |
| 33 | for _ in 0..<20 { |
| 34 | XCTAssertTrue(model.enqueue(["op": .string("display_edit"), "at": .number(0), "length": .number(0), "text": .string("X")])) |
| 35 | while model.hasPendingInput { try await Task.sleep(for: .milliseconds(1)) } |
| 36 | model.didRender(model.projectionGeneration) |
| 37 | try await Task.sleep(for: .milliseconds(50)) |
| 38 | } |
| 39 | let finishedTyping = ContinuousClock.now |
| 40 | let deadline = finishedTyping.advanced(by: .seconds(15)) |
| 41 | while model.hasUnsavedChanges && ContinuousClock.now < deadline { |
| 42 | await model.flush() |
| 43 | try await Task.sleep(for: .milliseconds(10)) |
| 44 | } |
| 45 | XCTAssertFalse(model.hasUnsavedChanges) |
| 46 | let lag = finishedTyping.duration(to: .now) |
| 47 | struct Stats: Decodable { let updates: [Data] } |
| 48 | let stats: Stats = try await client.request("/stats") |
| 49 | print("SYNC: 20 edits, 250ms/request, uploads=\(stats.updates.count), catch-up after typing=\(lag)") |
| 50 | XCTAssertLessThanOrEqual(stats.updates.count, 10, "Unsent edits must share a request") |
| 51 | XCTAssertLessThan(lag, .seconds(1), "Typing must not leave a growing serial-request backlog") |
| 52 | let (initial, _) = try await client.response("/d/fixture/updates") |
| 53 | let peer = CRDTReplica(guid: "sync-fixture") |
| 54 | _ = try await peer.open(update: initial) |
| 55 | for update in stats.updates { _ = try await peer.apply(update) } |
| 56 | let projection = try await peer.projection() |
| 57 | XCTAssertEqual(projection.children.map(\.plainText), model.projection?.children.map(\.plainText), "A receiver must get every edit") |
| 58 | await peer.close() |
| 59 | await model.stop() |
| 60 | } |
| 61 | } |
| 62 | |