// AsyncCallableTests.swift // Covers the GIO async pattern binding: `planAsyncPairs` recognizing a // `*_async`/`*_finish` pair and `renderAsyncMethod` emitting the paired // Swift `async` method. Mirrors `CallbackGenerationTests.swift`'s style — // exercising the planner through its public entry points and the renderer // through `renderModule`'s public file output. import Testing @testable import GObjectGeneratorCore @Suite("GIO async pattern") struct AsyncCallableTests { // MARK: - Fixture /// A minimal `Gio`-module fixture: an `AsyncResult` class, a `Widget` /// class with one properly-shaped `foo_async`/`foo_finish` pair, and /// the `Gio.AsyncReadyCallback` callback type the pair's starter uses. /// Modeled entirely inside "Gio" itself (rather than a separate /// dependent module) so rule 6 (`currentModule == "Gio"`) is satisfied /// trivially, keeping the fixture minimal. func makeFooAsyncClass(includeFinish: Bool = true) -> Class { var methods = [ Method( name: "foo_async", cIdentifier: "g_widget_foo_async", parameters: [ Parameter(name: "self", type: .typeRef("Widget"), isInstanceParameter: true), Parameter(name: "callback", type: .typeRef("AsyncReadyCallback", namespace: "Gio"), cType: "GAsyncReadyCallback", isNullable: true), Parameter(name: "user_data", type: .pointer, cType: "gpointer", isNullable: true), ], returnValue: ReturnValue() ), ] if includeFinish { methods.append(Method( name: "foo_finish", cIdentifier: "g_widget_foo_finish", parameters: [ Parameter(name: "self", type: .typeRef("Widget"), isInstanceParameter: true), Parameter(name: "result", type: .typeRef("AsyncResult", namespace: "Gio"), cType: "GAsyncResult*"), ], returnValue: ReturnValue(type: .boolean), throwsGError: true )) } return Class(name: "Widget", cType: "GWidget", parent: nil, getTypeFunction: "g_widget_get_type", methods: methods) } func makeContext(includeFinish: Bool = true) -> (context: MapContext, klass: Class) { let klass = makeFooAsyncClass(includeFinish: includeFinish) let gio = Repository(namespaces: [ Namespace( name: "Gio", version: "2.0", classes: [ Class(name: "AsyncResult", cType: "GAsyncResult", parent: nil, getTypeFunction: "g_async_result_get_type"), klass, ], callbacks: [Callback( name: "AsyncReadyCallback", cType: "GAsyncReadyCallback", parameters: [ Parameter(name: "source_object", type: .pointer, cType: "gpointer", isNullable: true), Parameter(name: "res", type: .typeRef("AsyncResult"), cType: "GAsyncResult*"), Parameter(name: "data", type: .pointer, cType: "gpointer", isNullable: true), ] )] ) ]) let registry = TypeRegistry(repositories: ["Gio": gio]) let context = MapContext(registry: registry, currentModule: "Gio", currentNamespace: "Gio") return (context, klass) } // MARK: - Recognizer @Test("A parsed foo_async + foo_finish pair produces one AsyncCallablePlan, and the starter's asyncRole parameters are the last two") func fooAsyncFooFinishProducesOnePair() throws { let (context, klass) = makeContext() let (plan, memberSkips) = planClass(klass, context: context) #expect(plan.asyncMethods.count == 1) let pair = try #require(plan.asyncMethods.first) #expect(pair.starter.name == "fooAsync") #expect(pair.finish.name == "fooFinish") // The starter's Swift-facing signature carries no callback/user-data // parameters — only the async-role pair, trailing. let realParams = pair.starter.parameters.filter { !$0.isInstanceParameter } #expect(realParams.count == 2) #expect(realParams[0].asyncRole == .callback) #expect(realParams[1].asyncRole == .userData) // No callable reaches methodPlans still skipped for the callback param. #expect(!plan.methods.contains { $0.name == "fooAsync" }) #expect(plan.methods.contains { $0.name == "fooFinish" }) // The stale D4.3 skip for the paired starter was dropped. #expect(!memberSkips.contains { $0.cIdentifier == "g_widget_foo_async" }) } @Test("foo_async with no foo_finish produces no pair and keeps its callbackWithoutUserData skip") func fooAsyncWithoutFinishStaysSkipped() throws { let (context, klass) = makeContext(includeFinish: false) let (plan, memberSkips) = planClass(klass, context: context) #expect(plan.asyncMethods.isEmpty) #expect(!plan.methods.contains { $0.name == "fooAsync" }) let skip = try #require(memberSkips.first { $0.cIdentifier == "g_widget_foo_async" }) #expect(skip.reason == .callbackWithoutUserData) } // MARK: - Renderer @Test("renderAsyncMethod output contains async throws, _sgtkAwaitAsyncReady, defer g_object_unref, and AsyncResultRef(retaining:)") func renderedAsyncMethodContainsExpectedFragments() throws { let (context, klass) = makeContext() let (plan, _) = planClass(klass, context: context) let module = ModulePlan(module: "Gio", types: [.class(plan)], skips: [], coverage: CoverageStats()) let files = renderModule(module) let source = try #require(files["Widget.swift"]) #expect(source.contains("public func fooAsync(") ) #expect(source.contains(") async throws")) #expect(source.contains("_sgtkAwaitAsyncReady")) #expect(source.contains("defer { g_object_unref(")) #expect(source.contains("AsyncResultRef(retaining:")) #expect(source.contains("try fooFinish(result:")) } @Test("swiftSignature for a starter omits the callback and user-data parameters") func starterSignatureOmitsAsyncRoleParameters() throws { let (context, klass) = makeContext() let (plan, _) = planClass(klass, context: context) let module = ModulePlan(module: "Gio", types: [.class(plan)], skips: [], coverage: CoverageStats()) let files = renderModule(module) let source = try #require(files["Widget.swift"]) // The starter takes no real parameters beyond the trailing async // pair, so its emitted signature is empty parens. #expect(source.contains("public func fooAsync() async throws")) #expect(!source.contains("callback:")) #expect(!source.contains("userData:")) } }