Bind the GIO async pattern as Swift async methods
This commit is contained in:
parent
2d75fa9d86
commit
298e5a4b43
19 changed files with 722 additions and 1398 deletions
146
Tests/GObjectGeneratorCoreTests/AsyncCallableTests.swift
Normal file
146
Tests/GObjectGeneratorCoreTests/AsyncCallableTests.swift
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
// 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:"))
|
||||
}
|
||||
}
|
||||
|
|
@ -123,6 +123,43 @@ struct CallbackGenerationTests {
|
|||
#expect(mapping.swiftType.hasSuffix("-> String"))
|
||||
}
|
||||
|
||||
// MARK: - Declaring-namespace resolution (GIO async pattern, Step 1)
|
||||
|
||||
@Test("A callback declared in namespace A whose parameter is a bare type in A, referenced from namespace B, maps successfully")
|
||||
func callbackResolvesOwnParametersAgainstDeclaringNamespace() throws {
|
||||
// Mirrors `Gio.AsyncReadyCallback`: Gio's own `<callback>` spells its
|
||||
// `res` parameter as a bare `<type name="AsyncResult"/>` (no
|
||||
// namespace prefix), but Gdk references the callback type itself as
|
||||
// `Gio.AsyncReadyCallback`. Resolving that bare `AsyncResult` against
|
||||
// the CALLER's namespace ("B" here, "Gdk" for real) instead of the
|
||||
// callback's OWN declaring namespace ("A" / "Gio") is exactly the bug
|
||||
// Step 1 fixes.
|
||||
let namespaceA = Repository(namespaces: [
|
||||
Namespace(
|
||||
name: "A", version: "1.0",
|
||||
enumerations: [
|
||||
Enumeration(name: "Status", cType: "AStatus", getTypeFunction: "a_status_get_type"),
|
||||
],
|
||||
callbacks: [Callback(
|
||||
name: "ReadyCallback", cType: "AReadyCallback",
|
||||
parameters: [
|
||||
Parameter(name: "status", type: .typeRef("Status"), cType: "AStatus"),
|
||||
]
|
||||
)]
|
||||
)
|
||||
])
|
||||
let namespaceB = Repository(
|
||||
namespaces: [Namespace(name: "B", version: "1.0")],
|
||||
includedPackages: [IncludeEntry(name: "A", version: "1.0")]
|
||||
)
|
||||
let registry = TypeRegistry(repositories: ["A": namespaceA, "B": namespaceB])
|
||||
let ctx = MapContext(registry: registry, currentModule: "B", currentNamespace: "B", dependencyModules: ["A"])
|
||||
let mapping = try map(.typeRef("ReadyCallback", namespace: "A"),
|
||||
nullable: false, transfer: .none, context: ctx)
|
||||
#expect(mapping.category == .callback)
|
||||
#expect(mapping.swiftType.contains("Status"))
|
||||
}
|
||||
|
||||
// MARK: - Renderer infrastructure (constructed directly — D4.3 render-side proof)
|
||||
|
||||
/// A `.callbackBox(scope: .call, …)` parameter that doubles as its own
|
||||
|
|
|
|||
|
|
@ -573,6 +573,37 @@ struct TypeMapperTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test("A gconstpointer-typed scalar buffer (no elementCType) maps to [UInt8]")
|
||||
func arrayParamGconstpointerBuffer() throws {
|
||||
// Mirrors g_bytes_new's `data` parameter:
|
||||
// <array length="1" zero-terminated="0" c:type="gconstpointer"><type name="guint8"/></array>
|
||||
// — the array's own c:type is the pointer TYPEDEF `gconstpointer`
|
||||
// (no literal `*`), and the element has no c:type of its own.
|
||||
let m = try mapArrayParameter(
|
||||
element: .uint8,
|
||||
info: ArrayInfo(lengthParameterIndex: 1, cType: "gconstpointer"),
|
||||
transfer: .none, context: makeContext())
|
||||
#expect(m.swiftType == "[UInt8]")
|
||||
#expect(m.marshalIn == .scalarArrayToC)
|
||||
}
|
||||
|
||||
@Test("A transfer=full gconstpointer-typed scalar buffer is rejected (the helper only lends a temporary copy)")
|
||||
func arrayParamGconstpointerBufferTransferFullRejected() {
|
||||
// Mirrors g_bytes_new_take's `data` parameter, transfer-ownership="full":
|
||||
// the C callee frees the buffer, but `_withScalarArray` only lends a
|
||||
// pointer into a temporary Swift copy for the call's duration.
|
||||
do {
|
||||
_ = try mapArrayParameter(
|
||||
element: .uint8,
|
||||
info: ArrayInfo(lengthParameterIndex: 1, cType: "gconstpointer"),
|
||||
transfer: .full, context: makeContext())
|
||||
Issue.record("Expected transfer=full scalar buffer to be rejected")
|
||||
} catch {
|
||||
#expect(error.reason == .arrayBridgingUnimplemented)
|
||||
#expect(error.detail.contains("transfer-ownership=full"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test("A zero-terminated scalar buffer with no length param is rejected")
|
||||
func arrayParamScalarWithoutLengthRejected() {
|
||||
do {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue