Skip to content

Commit

Permalink
updating Integration tests, and adding explicit example from issue 106
Browse files Browse the repository at this point in the history
  • Loading branch information
heckj committed Jul 2, 2024
1 parent 4528511 commit 74e837e
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ extension RepoPeer2PeerIntegrationTests {
try await p2pAlice.startListening(as: "Alice")

// add the document to the Alice repo
let handle: DocHandle = try await repoAlice.create(doc: Document(), id: DocumentId())
let handle: DocHandle = try await repoAlice.create(id: DocumentId())
try addContent(handle.doc)

let repoBob = Repo(sharePolicy: SharePolicy.agreeable)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ extension RepoPeer2PeerIntegrationTests {
try await p2pBob.startListening(as: "Bob")

// add the document to the Alice repo
let handle: DocHandle = try await repoAlice.create(doc: Document(), id: DocumentId())
let handle: DocHandle = try await repoAlice.create(id: DocumentId())
try addContent(handle.doc)

// With the websocket protocol, we don't get confirmation of a sync being complete -
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ extension RepoPeer2PeerIntegrationTests {
try await p2pBob.startListening(as: "Bob")

// add the document to the Alice repo
let handle: DocHandle = try await repoAlice.create(doc: Document(), id: DocumentId())
let handle: DocHandle = try await repoAlice.create(id: DocumentId())
try addContent(handle.doc)

// With the websocket protocol, we don't get confirmation of a sync being complete -
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ extension RepoPeer2PeerIntegrationTests {
try await p2pBob.startListening(as: "Bob")

// add the document to the Alice repo
let handle: DocHandle = try await repoAlice.create(doc: Document(), id: DocumentId())
let handle: DocHandle = try await repoAlice.create(id: DocumentId())
try addContent(handle.doc)

// With the websocket protocol, we don't get confirmation of a sync being complete -
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ extension RepoPeer2PeerIntegrationTests {
try await p2pBob.startListening(as: "Bob")

// add the document to the Alice repo
let handle: DocHandle = try await repoAlice.create(doc: Document(), id: DocumentId())
let handle: DocHandle = try await repoAlice.create(id: DocumentId())
try addContent(handle.doc)

// With the websocket protocol, we don't get confirmation of a sync being complete -
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import Automerge
import AutomergeRepo
import AutomergeUtilities
import OSLog
import XCTest
import Combine

// NOTE(heckj): This integration test expects that you have a websocket server with the
// Automerge-repo sync protocol running at localhost:3030. If you're testing from the local
// repository, run the `./scripts/interop.sh` script to start up a local instance to
// respond.

final class RepoAndTwoClientWebsocketIntegrationTests: XCTestCase {
private static let subsystem = Bundle.main.bundleIdentifier!

static let test = Logger(subsystem: subsystem, category: "WebSocketSyncIntegrationTests")
let syncDestination = "ws://localhost:3030/"
// Switch to the following line to run a test against the public hosted automerge-repo instance
// let syncDestination = "wss://sync.automerge.org/"

// document structure for test
struct ExampleStruct: Identifiable, Codable, Hashable {
let id: UUID
var title: String
var discussion: AutomergeText

init(title: String, discussion: String) {
id = UUID()
self.title = title
self.discussion = AutomergeText(discussion)
}
}

override func setUp() async throws {
let isWebSocketConnectable = await webSocketAvailable(destination: syncDestination)
try XCTSkipUnless(isWebSocketConnectable, "websocket unavailable for integration test")
}

override func tearDown() async throws {
// teardown
}

func webSocketAvailable(destination: String) async -> Bool {
guard let url = URL(string: destination) else {
Self.test.error("invalid URL: \(destination, privacy: .public) - endpoint unavailable")
return false
}
// establishes the websocket
let request = URLRequest(url: url)
let ws: URLSessionWebSocketTask = URLSession.shared.webSocketTask(with: request)
ws.resume()
Self.test.info("websocket to \(destination, privacy: .public) prepped, sending ping")
do {
try await ws.sendPing()
Self.test.info("PING OK - returning true")
ws.cancel(with: .normalClosure, reason: nil)
return true
} catch {
Self.test.error("PING FAILED: \(error.localizedDescription, privacy: .public) - returning false")
ws.cancel(with: .abnormalClosure, reason: nil)
return false
}
}

// MARK: Utilities for the test

func newConnectedRepo() async throws -> (Repo, WebSocketProvider) {
// set up repo (with a client-websocket)
let repo = Repo(sharePolicy: SharePolicy.agreeable)
let websocket = WebSocketProvider()
await repo.addNetworkAdapter(adapter: websocket)

// establish connection to remote automerge-repo instance over a websocket
let url = try XCTUnwrap(URL(string: syncDestination))
try await websocket.connect(to: url)
return (repo, websocket)
}

func createAndStoreDocument(_ id: DocumentId, repo: Repo) async throws -> (DocHandle, [ChangeHash]) {
// add the document to the repo
let handle: DocHandle = try await repo.create(id: id)

// initial setup and encoding of Automerge doc to sync it
let encoder = AutomergeEncoder(doc: handle.doc)
let model = ExampleStruct(title: "new item", discussion: "editable text")
try encoder.encode(model)

// With the websocket protocol, we don't get confirmation of a sync being complete -
// if the other side has everything and nothing new, they just won't send a response
// back. In that case, we don't get any further responses - but we don't _know_ that
// it's complete. In an initial sync there will always be at least one response, but
// we can't quite count on this always being an initial sync... so I'm shimming in a
// short "wait" here to leave the background tasks that receive WebSocket messages
// running to catch any updates, and hoping that'll be enough time to complete it.
try await Task.sleep(for: .seconds(5))
let history = handle.doc.getHistory()
return (handle, history)
}

// MARK: The Tests

func testIssue106_history() async throws {
// stepping into details from https://github.com/automerge/automerge-repo-swift/issues/106
// History on the document as soon as it's returned should be equivalent. There should be
// no need to wait for any change notifications.
let documentIdForTest = DocumentId()
let (repoA, websocketA) = try await newConnectedRepo()
let (_, historyFromCreatedDoc) = try await createAndStoreDocument(documentIdForTest, repo: repoA)

// now establish a new connection, representing a second peer, looking for the data
let (repoB, websocketB) = try await newConnectedRepo()

let handle = try await repoB.find(id: documentIdForTest)
let historyFromFoundDoc = handle.doc.getHistory()
XCTAssertEqual(historyFromCreatedDoc, historyFromFoundDoc)

// cleanup
await websocketA.disconnect()
await websocketB.disconnect()
}

func testIssue106_notificationOnChange() async throws {
let documentIdForTest = DocumentId()
let (repoA, websocketA) = try await newConnectedRepo()
let (createdDocHandle, historyFromCreatedDoc) = try await createAndStoreDocument(documentIdForTest, repo: repoA)

// now establish a new connection, representing a second peer, looking for the data
let (repoB, websocketB) = try await newConnectedRepo()

let handle = try await repoB.find(id: documentIdForTest)
let historyFromFoundDoc = handle.doc.getHistory()
XCTAssertEqual(historyFromCreatedDoc, historyFromFoundDoc)

// set up expectation to await for trigger from the objectWillChange publisher on the "found" doc
let documentChangePublisherExpectation = expectation(description: "Document handle from repo 'B' receives a change when the document handle from Repo 'A' is updated")
let a = handle.doc.objectWillChange.receive(on: DispatchQueue.main).sink { peerList in
documentChangePublisherExpectation.fulfill()
}
XCTAssertNotNil(a)
// This is loosely the equivalent of the code provided in the issue, but without the prepend
// handle.doc.objectWillChange.prepend(()).receive(on: DispatchQueue.main).sink {
// print("\(id) history count: \(handle.doc.getHistory().count)")
// }
// .store(in: &subs)

// make a change
let encoder = AutomergeEncoder(doc: createdDocHandle.doc)
let model = ExampleStruct(title: "updated item", discussion: "editable text")
try encoder.encode(model)
// encoding writes into the document, which should initiate the change...

await fulfillment(of: [documentChangePublisherExpectation], timeout: expectationTimeOut, enforceOrder: false)

// and afterwards, their histories should be identical as well.
XCTAssertEqual(createdDocHandle.doc.getHistory(), handle.doc.getHistory())
// cleanup
await websocketA.disconnect()
await websocketB.disconnect()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ final class RepoWebsocketIntegrationTests: XCTestCase {
await repo.addNetworkAdapter(adapter: websocket)

// add the document to the repo
let handle: DocHandle = try await repo.create(doc: Document(), id: DocumentId())
let handle: DocHandle = try await repo.create(id: DocumentId())

// initial setup and encoding of Automerge doc to sync it
let encoder = AutomergeEncoder(doc: handle.doc)
Expand Down

0 comments on commit 74e837e

Please sign in to comment.