diff --git a/.github/workflows/ios-rust.yml b/.github/workflows/ios-rust.yml index 66290d7..ea3dc49 100644 --- a/.github/workflows/ios-rust.yml +++ b/.github/workflows/ios-rust.yml @@ -21,6 +21,8 @@ jobs: run: cargo test --workspace --all-features - name: Fake iOS FFI tests run: cargo test -p tele-ios-ffi --no-default-features --features standalone-fake + - name: Swift FFI smoke + run: scripts/smoke-ios-ffi-swift.sh /tmp/tele-ios-ffi-swift-smoke - name: Generate iOS FFI bindings run: scripts/generate-ios-ffi-bindings.sh /tmp/tele-ios-ffi - name: Swift bindings typecheck diff --git a/crates/tele-ios-ffi/README.md b/crates/tele-ios-ffi/README.md index 3d8df73..cdacc2a 100644 --- a/crates/tele-ios-ffi/README.md +++ b/crates/tele-ios-ffi/README.md @@ -24,6 +24,12 @@ Build the fake-only iOS simulator XCFramework without linking TDLib: DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer scripts/build-ios-fake-ffi-xcframework.sh ``` +Run an executable Swift smoke test against matching fake-only UniFFI bindings: + +```bash +DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer scripts/smoke-ios-ffi-swift.sh +``` + Current linking status: - Xcode is installed at `/Applications/Xcode.app`, and `DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcodebuild -version` reports Xcode 26.5. diff --git a/scripts/smoke-ios-ffi-swift.sh b/scripts/smoke-ios-ffi-swift.sh new file mode 100755 index 0000000..7f92866 --- /dev/null +++ b/scripts/smoke-ios-ffi-swift.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +out_dir="${1:-/private/tmp/tele-ios-ffi-swift-smoke}" +lib_path="${repo_root}/target/release/libtele_ios_ffi.a" + +if [[ -z "${DEVELOPER_DIR:-}" && -d /Applications/Xcode.app/Contents/Developer ]]; then + export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer +fi + +cd "${repo_root}" + +case "${out_dir}" in + "" | "/" | "/tmp" | "/private" | "/private/tmp") + printf 'Refusing unsafe output directory: %s\n' "${out_dir}" >&2 + exit 2 + ;; +esac + +cargo build \ + -p tele-ios-ffi \ + --no-default-features \ + --features standalone-fake \ + --release + +rm -rf "${out_dir}" +mkdir -p "${out_dir}/Swift" "${out_dir}/Headers" + +cargo run -p uniffi-bindgen-swift -- "${lib_path}" "${out_dir}/Swift" --swift-sources +cargo run -p uniffi-bindgen-swift -- "${lib_path}" "${out_dir}/Headers" --headers +cargo run -p uniffi-bindgen-swift -- "${lib_path}" "${out_dir}/Headers" \ + --modulemap \ + --module-name tele_ios_ffiFFI \ + --modulemap-filename module.modulemap + +cat > "${out_dir}/Smoke.swift" <<'SWIFT' +import Foundation + +@main +struct Smoke { + static func main() throws { + func require(_ condition: @autoclosure () -> Bool, _ message: String) { + if !condition() { + fatalError(message) + } + } + + let session = try createSession(config: IosSessionConfig( + accountId: "fake", + displayName: "Fake", + databasePath: "/tmp/tele-ios-ffi-swift-smoke-db", + useFakeTdlib: true + )) + + let chats = try session.loadChats(limit: 20) + require(chats.count == 1, "expected one fake chat") + let chat = chats[0] + + let history = try session.loadHistory(chatId: chat.id, limit: 20) + require(history.first?.text == "Hello from fake TDLib", "expected seeded history") + + let sent = try session.sendMessage(chatId: chat.id, text: "Hi from Swift FFI", replyToMessageId: nil) + require(sent.text == "Hi from Swift FFI", "expected sent message text") + + let copiedText = try session.copyPayload(chatId: chat.id, messageId: sent.id) + require(copiedText == "Hi from Swift FFI", "expected copy payload") + + let reactions = try session.react(chatId: chat.id, messageId: sent.id, reaction: "+1") + require(reactions.first?.emoji == "+1", "expected reaction") + + let results = try session.searchMessages(chatId: chat.id, query: "Swift FFI") + require(results.count == 1, "expected search result") + + session.simulateIncomingMessage(chatId: chat.id, text: "Incoming from Swift smoke", senderName: "Bob") + let events = session.pollEvents() + require(events.contains { if case .messageAdded = $0 { return true }; return false }, "expected messageAdded event") + require(events.contains { if case .incomingNotificationCandidate = $0 { return true }; return false }, "expected notification candidate") + + print("tele-ios-ffi Swift smoke passed") + } +} +SWIFT + +swiftc \ + -parse-as-library \ + -module-cache-path "${out_dir}/module-cache" \ + -I "${out_dir}/Headers" \ + "${out_dir}/Swift/tele_ios_ffi.swift" \ + "${out_dir}/Smoke.swift" \ + "${lib_path}" \ + -o "${out_dir}/smoke" + +"${out_dir}/smoke"