1
0
Fork 0
gobject-generator/Tests/GObjectGeneratorCoreTests/AsyncCallableTests.swift

146 lines
6.8 KiB
Swift

// 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:"))
}
}