598 lines
30 KiB
Swift
598 lines
30 KiB
Swift
// RendererCallableTests.swift
|
|
// Renderer-level tests for callable features that the compiler gate alone
|
|
// cannot catch: throws signatures, out-param tuple ordering, constructor
|
|
// error handling, and the memory-safety caller-allocates skip.
|
|
|
|
import Testing
|
|
|
|
@testable import GObjectGeneratorCore
|
|
|
|
@Suite("Renderer callables")
|
|
struct RendererCallableTests {
|
|
func makeContext() -> MapContext {
|
|
let gobject = Repository(namespaces: [
|
|
Namespace(
|
|
name: "GObject", version: "2.0",
|
|
classes: [
|
|
Class(name: "Object", cType: "GObject", parent: nil,
|
|
getTypeFunction: "g_object_get_type"),
|
|
Class(name: "InitiallyUnowned", cType: "GInitiallyUnowned",
|
|
parent: "Object",
|
|
getTypeFunction: "g_initially_unowned_get_type"),
|
|
]
|
|
)
|
|
])
|
|
let registry = TypeRegistry(repositories: ["GObject": gobject])
|
|
return MapContext(registry: registry, currentModule: "GObject", currentNamespace: "GObject")
|
|
}
|
|
/// Builds a context with an ordinary GObject and a non-GObject
|
|
/// fundamental class that declares its own ref function.
|
|
func makeTransferContext() -> MapContext {
|
|
let gobject = Repository(namespaces: [
|
|
Namespace(
|
|
name: "GObject", version: "2.0",
|
|
classes: [
|
|
Class(name: "Object", cType: "GObject", parent: nil,
|
|
getTypeFunction: "g_object_get_type"),
|
|
Class(name: "Expr", cType: "GExpr", parent: nil,
|
|
getTypeFunction: "g_expr_get_type",
|
|
refFunc: "g_expr_ref", unrefFunc: "g_expr_unref"),
|
|
]
|
|
)
|
|
])
|
|
let registry = TypeRegistry(repositories: ["GObject": gobject])
|
|
return MapContext(registry: registry, currentModule: "GObject", currentNamespace: "GObject")
|
|
}
|
|
|
|
func renderCallable(_ plan: CallablePlan) -> String {
|
|
let module = ModulePlan(
|
|
module: "GObject",
|
|
types: [.callable(plan)],
|
|
skips: [],
|
|
coverage: CoverageStats()
|
|
)
|
|
return renderModule(module)["Functions.swift"] ?? ""
|
|
}
|
|
|
|
// MARK: - C2: Throws + result! unwrap
|
|
|
|
@Test("Throwing function returning object emits throws, &error, and result!")
|
|
func throwingFunctionEmitsResultUnwrap() throws {
|
|
// A function that returns Object and throws.
|
|
let fn = GlobalFunction(
|
|
name: "load_object", cIdentifier: "g_load_object",
|
|
parameters: [Parameter(name: "path", type: .string, cType: "const char*")],
|
|
returnValue: ReturnValue(type: .typeRef("Object", namespace: "GObject")),
|
|
throwsGError: true
|
|
)
|
|
guard case .success(let plan) = planFunction(fn, context: makeContext()) else {
|
|
Issue.record("expected load_object to plan successfully"); return
|
|
}
|
|
let body = renderCallable(plan)
|
|
#expect(body.contains("throws"))
|
|
#expect(body.contains("&error"))
|
|
// Pointer-returning throwing function: result! unwrap before adoption
|
|
#expect(body.contains("result!") || body.contains("marshalReturn"))
|
|
}
|
|
|
|
// MARK: - Phase E4: no-unsafe-pointer public API policy
|
|
|
|
@Test("A function with a gpointer parameter renders @_spi(SGTKInternal) public func, hiding the pointer")
|
|
func gpointerParameterHidesFunctionBehindSPI() throws {
|
|
let fn = GlobalFunction(
|
|
name: "set_user_data", cIdentifier: "g_set_user_data",
|
|
parameters: [Parameter(name: "data", type: .pointer, cType: "gpointer")],
|
|
returnValue: ReturnValue(type: .void)
|
|
)
|
|
guard case .success(let plan) = planFunction(fn, context: makeContext()) else {
|
|
Issue.record("expected set_user_data to plan successfully"); return
|
|
}
|
|
let body = renderCallable(plan)
|
|
#expect(body.contains("@_spi(SGTKInternal) public func setUserData"))
|
|
}
|
|
|
|
@Test("A function returning a plain wrapper type (no raw pointer in its signature) stays plain public")
|
|
func wrapperReturnStaysPlainPublic() throws {
|
|
let fn = GlobalFunction(
|
|
name: "get_default_object", cIdentifier: "g_get_default_object",
|
|
parameters: [],
|
|
returnValue: ReturnValue(type: .typeRef("Object", namespace: "GObject"))
|
|
)
|
|
guard case .success(let plan) = planFunction(fn, context: makeContext()) else {
|
|
Issue.record("expected get_default_object to plan successfully"); return
|
|
}
|
|
let body = renderCallable(plan)
|
|
#expect(body.contains("public func getDefaultObject"))
|
|
#expect(!body.contains("@_spi(SGTKInternal) public func getDefaultObject"))
|
|
}
|
|
|
|
// MARK: - transfer-ownership="full" object in-parameters
|
|
|
|
@Test("transfer-full object in-param refs before the call; transfer-none does not")
|
|
func transferFullObjectParamAddsRef() throws {
|
|
let ctx = makeTransferContext()
|
|
let borrowed = GlobalFunction(
|
|
name: "set_thing", cIdentifier: "g_set_thing",
|
|
parameters: [Parameter(name: "thing", type: .typeRef("Object", namespace: "GObject"),
|
|
cType: "GObject*", transferOwnership: .none)],
|
|
returnValue: ReturnValue(type: .void))
|
|
let consumed = GlobalFunction(
|
|
name: "take_thing", cIdentifier: "g_take_thing",
|
|
parameters: [Parameter(name: "thing", type: .typeRef("Object", namespace: "GObject"),
|
|
cType: "GObject*", transferOwnership: .full)],
|
|
returnValue: ReturnValue(type: .void))
|
|
guard case .success(let borrowedPlan) = planFunction(borrowed, context: ctx),
|
|
case .success(let consumedPlan) = planFunction(consumed, context: ctx) else {
|
|
Issue.record("expected both functions to plan successfully")
|
|
return
|
|
}
|
|
#expect(renderCallable(borrowedPlan).contains(
|
|
"g_set_thing(_instancePointer(thing.pointer))"))
|
|
#expect(renderCallable(consumedPlan).contains(
|
|
"g_take_thing(_instancePointer(_rawPointer(g_object_ref(_instancePointer(thing.pointer)))))"))
|
|
}
|
|
|
|
@Test("transfer-full nullable object in-param refs inside the map closure")
|
|
func transferFullNullableObjectParamRefsInsideMap() throws {
|
|
let fn = GlobalFunction(
|
|
name: "take_maybe", cIdentifier: "g_take_maybe",
|
|
parameters: [Parameter(name: "thing", type: .typeRef("Object", namespace: "GObject"),
|
|
cType: "GObject*", isNullable: true, transferOwnership: .full)],
|
|
returnValue: ReturnValue(type: .void))
|
|
guard case .success(let plan) = planFunction(fn, context: makeTransferContext()) else {
|
|
Issue.record("expected g_take_maybe to plan successfully")
|
|
return
|
|
}
|
|
#expect(renderCallable(plan).contains(
|
|
"thing.map { _instancePointer(_rawPointer(g_object_ref(_instancePointer($0.pointer)))) }"))
|
|
}
|
|
|
|
@Test("transfer-full param of a non-GObject fundamental uses its own ref function")
|
|
func transferFullFundamentalUsesOwnRefFunction() throws {
|
|
let fn = GlobalFunction(
|
|
name: "take_expr", cIdentifier: "g_take_expr",
|
|
parameters: [Parameter(name: "expr", type: .typeRef("Expr", namespace: "GObject"),
|
|
cType: "GExpr*", transferOwnership: .full)],
|
|
returnValue: ReturnValue(type: .void))
|
|
guard case .success(let plan) = planFunction(fn, context: makeTransferContext()) else {
|
|
Issue.record("expected g_take_expr to plan successfully")
|
|
return
|
|
}
|
|
let body = renderCallable(plan)
|
|
#expect(body.contains(
|
|
"g_take_expr(_instancePointer(_rawPointer(g_expr_ref(_instancePointer(expr.pointer)))))"))
|
|
#expect(!body.contains("g_object_ref"))
|
|
}
|
|
|
|
// MARK: - C3: Out-param tuples
|
|
|
|
@Test("Single out-param with no Swift return becomes the out-param's type")
|
|
func singleOutParamNoReturn() throws {
|
|
// A function that returns void but has one out-param.
|
|
let fn = GlobalFunction(
|
|
name: "get_data", cIdentifier: "g_get_data",
|
|
parameters: [
|
|
Parameter(name: "key", type: .string, cType: "const char*"),
|
|
Parameter(name: "result", type: .string, cType: "char**", direction: .out),
|
|
],
|
|
returnValue: ReturnValue(type: .void)
|
|
)
|
|
guard case .success(let plan) = planFunction(fn, context: makeContext()) else {
|
|
Issue.record("expected get_data to plan successfully"); return
|
|
}
|
|
let body = renderCallable(plan)
|
|
// Single out-param return: the type directly (no tuple label)
|
|
#expect(body.contains("func getData(key: String) -> String"))
|
|
}
|
|
|
|
@Test("Multiple out-params with no Swift return produce a tuple")
|
|
func multiOutParamNoReturnTuple() throws {
|
|
let fn = GlobalFunction(
|
|
name: "get_coords", cIdentifier: "g_get_coords",
|
|
parameters: [
|
|
Parameter(name: "id", type: .int32, cType: "gint"),
|
|
Parameter(name: "x", type: .int32, cType: "gint", direction: .out),
|
|
Parameter(name: "y", type: .int32, cType: "gint", direction: .out),
|
|
],
|
|
returnValue: ReturnValue(type: .void)
|
|
)
|
|
guard case .success(let plan) = planFunction(fn, context: makeContext()) else {
|
|
Issue.record("expected get_coords to plan successfully"); return
|
|
}
|
|
let body = renderCallable(plan)
|
|
#expect(body.contains("func getCoords(id: Int32) -> (x: Int32, y: Int32)"))
|
|
}
|
|
|
|
@Test("Out-param local type follows the c:type: mutable char** vs const char**")
|
|
func outParamLocalTypeFollowsCType() throws {
|
|
let mutableFn = GlobalFunction(
|
|
name: "next_token", cIdentifier: "g_next_token",
|
|
parameters: [
|
|
Parameter(name: "input", type: .string, cType: "const char*"),
|
|
Parameter(name: "endptr", type: .string, cType: "char**", direction: .out),
|
|
],
|
|
returnValue: ReturnValue(type: .void)
|
|
)
|
|
guard case .success(let mutablePlan) = planFunction(mutableFn, context: makeContext()) else {
|
|
Issue.record("expected next_token to plan successfully"); return
|
|
}
|
|
#expect(renderCallable(mutablePlan).contains("UnsafeMutablePointer<CChar>?"))
|
|
|
|
let constFn = GlobalFunction(
|
|
name: "peek_token", cIdentifier: "g_peek_token",
|
|
parameters: [
|
|
Parameter(name: "input", type: .string, cType: "const char*"),
|
|
Parameter(name: "endptr", type: .string, cType: "const char**", direction: .out),
|
|
],
|
|
returnValue: ReturnValue(type: .void)
|
|
)
|
|
guard case .success(let constPlan) = planFunction(constFn, context: makeContext()) else {
|
|
Issue.record("expected peek_token to plan successfully"); return
|
|
}
|
|
#expect(renderCallable(constPlan).contains("UnsafePointer<CChar>?"))
|
|
#expect(!renderCallable(constPlan).contains("UnsafeMutablePointer<CChar>?"))
|
|
}
|
|
|
|
@Test("Swift return with out-params produces a labeled tuple")
|
|
func returnWithOutParamsLabeledTuple() throws {
|
|
let fn = GlobalFunction(
|
|
name: "find_item", cIdentifier: "g_find_item",
|
|
parameters: [
|
|
Parameter(name: "id", type: .int32, cType: "gint"),
|
|
Parameter(name: "index", type: .int32, cType: "gint", direction: .out),
|
|
],
|
|
returnValue: ReturnValue(type: .boolean)
|
|
)
|
|
guard case .success(let plan) = planFunction(fn, context: makeContext()) else {
|
|
Issue.record("expected find_item to plan successfully"); return
|
|
}
|
|
let body = renderCallable(plan)
|
|
#expect(body.contains("-> (return: Bool, index: Int32)"))
|
|
}
|
|
|
|
// MARK: - Memory-safety caller-allocates skip
|
|
|
|
@Test("Caller-allocates out-param is skipped with the callerAllocates reason")
|
|
func callerAllocatesSkip() throws {
|
|
let fn = GlobalFunction(
|
|
name: "unichar_to_utf8", cIdentifier: "g_unichar_to_utf8",
|
|
parameters: [
|
|
Parameter(name: "c", type: .unichar, cType: "gunichar"),
|
|
Parameter(name: "outbuf", type: .string, cType: "char*", direction: .out,
|
|
callerAllocates: true),
|
|
]
|
|
)
|
|
guard case .skip(let entry) = planFunction(fn, context: makeContext()) else {
|
|
Issue.record("expected caller-allocates function to be skipped"); return
|
|
}
|
|
#expect(entry.reason == .outParameter)
|
|
#expect(entry.detail.contains("caller-allocates"))
|
|
}
|
|
|
|
// MARK: - Throwing constructor
|
|
|
|
@Test("Throwing constructor emits throws, &error, GLibError, and self.init(takingOwnership:)")
|
|
func throwingConstructorEmitsErrorHandling() throws {
|
|
// Construct a class with a throwing constructor.
|
|
let klass = Class(
|
|
name: "File", cType: "GFile", parent: "Object",
|
|
getTypeFunction: "g_file_get_type",
|
|
constructors: [
|
|
Constructor(name: "new", cIdentifier: "g_file_new",
|
|
parameters: [
|
|
Parameter(name: "path", type: .string, cType: "const char*"),
|
|
],
|
|
returnValue: ReturnValue(
|
|
transferOwnership: .full),
|
|
throwsGError: true),
|
|
]
|
|
)
|
|
let (plan, _) = planClass(klass, context: makeContext())
|
|
let module = ModulePlan(module: "GObject", types: [.class(plan)], skips: [],
|
|
coverage: CoverageStats())
|
|
let files = renderModule(module)
|
|
let src = files["File.swift"] ?? ""
|
|
#expect(src.contains("convenience init(path: String) throws"))
|
|
#expect(src.contains("&error"))
|
|
#expect(src.contains("throw GLibError(consuming: e)"))
|
|
#expect(src.contains("self.init(takingOwnership:"))
|
|
}
|
|
|
|
// MARK: - Out-param constructor skip
|
|
|
|
@Test("Constructor with out-params is skipped with constructorOutParams")
|
|
func outParamConstructorSkipped() throws {
|
|
let klass = Class(
|
|
name: "Stream", cType: "GStream", parent: "Object",
|
|
getTypeFunction: "g_stream_get_type",
|
|
constructors: [
|
|
Constructor(name: "new", cIdentifier: "g_stream_new",
|
|
parameters: [
|
|
Parameter(name: "name", type: .string, cType: "const char*"),
|
|
Parameter(name: "fd", type: .int32, cType: "gint",
|
|
direction: .out),
|
|
],
|
|
returnValue: ReturnValue(
|
|
transferOwnership: .full)),
|
|
]
|
|
)
|
|
let (plan, skips) = planClass(klass, context: makeContext())
|
|
// Constructor should be skipped — no constructor plan, skip entry recorded.
|
|
#expect(plan.constructors.isEmpty)
|
|
let outSkip = skips.first { $0.reason == .constructorOutParams }
|
|
#expect(outSkip != nil)
|
|
}
|
|
// MARK: - E1: throwing constructor with string params doesn't nest self.init
|
|
|
|
@Test("Throwing constructor with a String param threads the result out of withCString instead of nesting self.init")
|
|
func throwingConstructorWithStringParamAvoidsNestedSelfInit() throws {
|
|
// `self.init` (a delegating initializer call) is illegal inside any
|
|
// closure, including a non-escaping `withCString` trailing closure.
|
|
// Regression guard for the tier-2 DBusConnection/DBusProxy bug where
|
|
// string-parameterised throwing constructors called self.init from
|
|
// inside the withCString closure.
|
|
let klass = Class(
|
|
name: "Proxy", cType: "GProxy", parent: "Object",
|
|
getTypeFunction: "g_proxy_get_type",
|
|
constructors: [
|
|
Constructor(name: "new_for_bus_sync", cIdentifier: "g_proxy_new_for_bus_sync",
|
|
parameters: [
|
|
Parameter(name: "name", type: .string, cType: "const char*"),
|
|
],
|
|
returnValue: ReturnValue(transferOwnership: .full),
|
|
throwsGError: true),
|
|
]
|
|
)
|
|
let (plan, _) = planClass(klass, context: makeContext())
|
|
let module = ModulePlan(module: "GObject", types: [.class(plan)], skips: [],
|
|
coverage: CoverageStats())
|
|
let src = renderModule(module)["Proxy.swift"] ?? ""
|
|
// The C call (returning the result) lives INSIDE the withCString
|
|
// closure as its trailing/implicit-return expression...
|
|
#expect(src.contains("g_proxy_new_for_bus_sync(cString0, &error)"))
|
|
// ...while `self.init` runs OUTSIDE every closure, never nested.
|
|
#expect(!src.contains("cString0, &error)\n self.init"))
|
|
let lines = src.components(separatedBy: "\n")
|
|
guard let selfInitLine = lines.first(where: { $0.contains("self.init(takingOwnership:") }) else {
|
|
Issue.record("expected a self.init(takingOwnership:) line"); return
|
|
}
|
|
// Indentation of 8 spaces == top-level init body scope, not nested
|
|
// one level inside the withCString closure (12 spaces).
|
|
#expect(selfInitLine.hasPrefix(" self.init"))
|
|
}
|
|
|
|
// MARK: - E1: constructor signature deduplication
|
|
|
|
@Test("Two constructors with the same rendered init signature but different Swift names dedup to one, keeping the first")
|
|
func constructorsWithIdenticalSignatureDedup() throws {
|
|
// `init(...)` never carries the constructor's Swift name (it always
|
|
// renders as bare `init`), so two GIR constructors mapping to
|
|
// DIFFERENT Swift names (`newFinish` vs `newForAddressFinish`) but
|
|
// the IDENTICAL parameter/throws/return shape still collide at
|
|
// `init(res:)`. Regression guard for the tier-2 DBusConnection bug.
|
|
let resParam = Parameter(name: "res", type: .typeRef("AsyncResult", namespace: "GObject"),
|
|
cType: "GAsyncResult*")
|
|
let klass = Class(
|
|
name: "Connection", cType: "GConnection", parent: "Object",
|
|
getTypeFunction: "g_connection_get_type",
|
|
constructors: [
|
|
Constructor(name: "new_finish", cIdentifier: "g_connection_new_finish",
|
|
parameters: [resParam],
|
|
returnValue: ReturnValue(transferOwnership: .full),
|
|
throwsGError: true),
|
|
Constructor(name: "new_for_address_finish", cIdentifier: "g_connection_new_for_address_finish",
|
|
parameters: [resParam],
|
|
returnValue: ReturnValue(transferOwnership: .full),
|
|
throwsGError: true),
|
|
]
|
|
)
|
|
let ctx = MapContext(
|
|
registry: TypeRegistry(repositories: ["GObject": Repository(namespaces: [
|
|
Namespace(name: "GObject", version: "2.0", classes: [
|
|
Class(name: "Object", cType: "GObject", parent: nil, getTypeFunction: "g_object_get_type"),
|
|
Class(name: "AsyncResult", cType: "GAsyncResult", parent: "Object",
|
|
getTypeFunction: "g_async_result_get_type"),
|
|
])
|
|
])]),
|
|
currentModule: "GObject", currentNamespace: "GObject"
|
|
)
|
|
let (plan, skips) = planClass(klass, context: ctx)
|
|
#expect(plan.constructors.count == 1)
|
|
#expect(plan.constructors[0].cIdentifier == "g_connection_new_finish")
|
|
let dedupSkip = skips.first { $0.reason == .nameCollision }
|
|
#expect(dedupSkip?.cIdentifier == "g_connection_new_for_address_finish")
|
|
|
|
let module = ModulePlan(module: "GObject", types: [.class(plan)], skips: [],
|
|
coverage: CoverageStats())
|
|
let src = renderModule(module)["Connection.swift"] ?? ""
|
|
let occurrences = src.components(separatedBy: "convenience init(res: AsyncResult) throws").count - 1
|
|
#expect(occurrences == 1)
|
|
}
|
|
|
|
// MARK: - E1: pointer out-param zero-initializes to nil, not 0
|
|
|
|
@Test("A gpointer out-param local variable initializes to nil, not the integer 0")
|
|
func pointerOutParamInitializesToNil() throws {
|
|
// `.direct` marshalIn covers both numeric out-params (`gint*` → `0`)
|
|
// and raw-pointer out-params (`gpointer*` → `UnsafeMutableRawPointer?`,
|
|
// which `= 0` cannot initialize). Regression guard for the tier-2
|
|
// FileInfo.getAttributeData bug.
|
|
let fn = GlobalFunction(
|
|
name: "get_attribute_data", cIdentifier: "g_file_info_get_attribute_data",
|
|
parameters: [
|
|
Parameter(name: "attribute", type: .string, cType: "const char*"),
|
|
Parameter(name: "value_pp", type: .pointer, cType: "gpointer*", direction: .out),
|
|
],
|
|
returnValue: ReturnValue(type: .void)
|
|
)
|
|
guard case .success(let plan) = planFunction(fn, context: makeContext()) else {
|
|
Issue.record("expected get_attribute_data to plan successfully"); return
|
|
}
|
|
let body = renderCallable(plan)
|
|
#expect(body.contains("var out0: UnsafeMutableRawPointer? = nil"))
|
|
#expect(!body.contains("var out0: UnsafeMutableRawPointer? = 0"))
|
|
}
|
|
|
|
// MARK: - E2: no unconditional Foundation import (collides with Gio.InputStream)
|
|
|
|
@Test("Generated module and support headers do not import Foundation")
|
|
func generatedHeadersOmitFoundationImport() throws {
|
|
let module = ModulePlan(module: "GLib", types: [], skips: [], coverage: CoverageStats())
|
|
let files = renderModule(module)
|
|
for (name, content) in files {
|
|
#expect(!content.contains("import Foundation"), "\(name) unexpectedly imports Foundation")
|
|
}
|
|
}
|
|
|
|
// MARK: - E3: nullable string constructor params
|
|
|
|
@Test("Constructor with nullable string parameter generates String? and _withOptionalCString")
|
|
func nullableStringConstructorParam() throws {
|
|
let klass = Class(
|
|
name: "Alert", cType: "GAlert", parent: "Object",
|
|
getTypeFunction: "g_alert_get_type",
|
|
constructors: [
|
|
Constructor(name: "new", cIdentifier: "g_alert_new",
|
|
parameters: [
|
|
Parameter(name: "message", type: .string,
|
|
cType: "const char*", isNullable: true),
|
|
],
|
|
returnValue: ReturnValue(transferOwnership: .full)),
|
|
]
|
|
)
|
|
let (plan, _) = planClass(klass, context: makeContext())
|
|
let module = ModulePlan(module: "GObject", types: [.class(plan)], skips: [],
|
|
coverage: CoverageStats())
|
|
let src = renderModule(module)["Alert.swift"] ?? ""
|
|
// The signature must show String? for the nullable string parameter.
|
|
#expect(src.contains("convenience init(message: String?)"))
|
|
// The body wraps the nullable param with _withOptionalCString.
|
|
#expect(src.contains("_withOptionalCString(message)"))
|
|
// The convenience init must NOT be SPI-gated (check the init line is not preceded by @_spi).
|
|
let initLines = src.components(separatedBy: "\n")
|
|
let convenienceLines = initLines.filter { $0.contains("convenience init") }
|
|
#expect(!convenienceLines.isEmpty, "expected a convenience init line")
|
|
for line in convenienceLines {
|
|
#expect(!line.contains("@_spi"), "convenience init should not be SPI-gated, got: \(line)")
|
|
}
|
|
// Uses takingOwnership for transfer-ownership="full".
|
|
if !src.contains("self.init(takingOwnership:") {
|
|
Issue.record("expected self.init(takingOwnership:) for transfer-ownership=full")
|
|
}
|
|
}
|
|
|
|
@Test("Constructor with mixed nullable and required string params generates correct bridging")
|
|
func constructorWithMixedNullableAndRequiredParams() throws {
|
|
let klass = Class(
|
|
name: "Dialog", cType: "GAlertDialog", parent: "Object",
|
|
getTypeFunction: "g_alert_dialog_get_type",
|
|
constructors: [
|
|
Constructor(name: "new", cIdentifier: "g_alert_dialog_new",
|
|
parameters: [
|
|
Parameter(name: "heading", type: .string,
|
|
cType: "const char*", isNullable: true),
|
|
Parameter(name: "body", type: .string,
|
|
cType: "const char*"),
|
|
],
|
|
returnValue: ReturnValue(transferOwnership: .full)),
|
|
]
|
|
)
|
|
let (plan, _) = planClass(klass, context: makeContext())
|
|
let module = ModulePlan(module: "GObject", types: [.class(plan)], skips: [],
|
|
coverage: CoverageStats())
|
|
let src = renderModule(module)["Dialog.swift"] ?? ""
|
|
// The signature must show String? for the nullable param and String for the required param.
|
|
#expect(src.contains("convenience init(heading: String?, body: String)"))
|
|
// The nullable param gets _withOptionalCString, the required param gets .withCString.
|
|
#expect(src.contains("_withOptionalCString(heading)"))
|
|
#expect(src.contains("body.withCString"))
|
|
// The convenience init line must NOT be SPI-gated (the class file has SPI-gated
|
|
// designated inits, but the convenience init itself is public).
|
|
let initLines2 = src.components(separatedBy: "\n")
|
|
let convenienceLines2 = initLines2.filter { $0.contains("convenience init") }
|
|
#expect(!convenienceLines2.isEmpty, "expected a convenience init line")
|
|
for line in convenienceLines2 {
|
|
#expect(!line.contains("@_spi"), "convenience init should not be SPI-gated, got: \(line)")
|
|
}
|
|
}
|
|
|
|
// MARK: - E3: constructor borrowing ownership
|
|
|
|
@Test("Constructor with ownership .none return generates convenience init calling self.init(retaining:)")
|
|
func constructorBorrowedReturnUsesRetaining() throws {
|
|
let klass = Class(
|
|
name: "Borrowed", cType: "GBorrowed", parent: "Object",
|
|
getTypeFunction: "g_borrowed_get_type",
|
|
constructors: [
|
|
Constructor(name: "new", cIdentifier: "g_borrowed_new",
|
|
parameters: [
|
|
Parameter(name: "name", type: .string,
|
|
cType: "const char*"),
|
|
],
|
|
returnValue: ReturnValue(transferOwnership: .none)),
|
|
]
|
|
)
|
|
let (plan, _) = planClass(klass, context: makeContext())
|
|
let module3 = ModulePlan(module: "GObject", types: [.class(plan)], skips: [],
|
|
coverage: CoverageStats())
|
|
let src = renderModule(module3)["Borrowed.swift"] ?? ""
|
|
#expect(src.contains("self.init(retaining:"), "expected self.init(retaining:) for transfer-ownership=none, got: \(src)")
|
|
#expect(!src.contains("self.init(takingOwnership:"), "should NOT use init(takingOwnership:) for transfer-ownership=none")
|
|
// The convenience init line must NOT be SPI-gated.
|
|
let initLines3 = src.components(separatedBy: "\n")
|
|
let convenienceLines3 = initLines3.filter { $0.contains("convenience init") }
|
|
for line in convenienceLines3 {
|
|
#expect(!line.contains("@_spi"), "convenience init should not be SPI-gated, got: \(line)")
|
|
}
|
|
}
|
|
|
|
// MARK: - C array in-parameter bridging
|
|
|
|
@Test("An object-pointer array renders _withPointerArray and elides the length parameter")
|
|
func objectArrayParameterRendersPointerArray() throws {
|
|
let fn = GlobalFunction(
|
|
name: "replace", cIdentifier: "g_replace",
|
|
parameters: [
|
|
Parameter(name: "pages",
|
|
type: .cArray(.typeRef("Object", namespace: "GObject"),
|
|
ArrayInfo(lengthParameterIndex: 1, cType: "GObject**",
|
|
elementCType: "GObject*")),
|
|
cType: "GObject**"),
|
|
Parameter(name: "n_pages", type: .int32, cType: "int"),
|
|
],
|
|
returnValue: ReturnValue(type: .void)
|
|
)
|
|
guard case .success(let plan) = planFunction(fn, context: makeContext()) else {
|
|
Issue.record("expected replace to plan successfully"); return
|
|
}
|
|
let body = renderCallable(plan)
|
|
#expect(body.contains("_withPointerArray(pages.map { $0.pointer })"))
|
|
#expect(body.contains("numericCast(pages.count)"))
|
|
#expect(body.contains("pages: [Object]"))
|
|
// The length arg is synthesized from `pages.count`, so it must not
|
|
// appear in the Swift signature.
|
|
#expect(!body.contains("nPages:"))
|
|
}
|
|
|
|
@Test("A const scalar buffer renders _withScalarArray and a typed Swift array")
|
|
func scalarArrayParameterRendersScalarArray() throws {
|
|
let fn = GlobalFunction(
|
|
name: "base64_encode", cIdentifier: "g_base64_encode",
|
|
parameters: [
|
|
Parameter(name: "data",
|
|
type: .cArray(.uint8, ArrayInfo(lengthParameterIndex: 1,
|
|
cType: "const guchar*")),
|
|
cType: "const guchar*"),
|
|
Parameter(name: "len", type: .size, cType: "gsize"),
|
|
],
|
|
returnValue: ReturnValue(type: .string)
|
|
)
|
|
guard case .success(let plan) = planFunction(fn, context: makeContext()) else {
|
|
Issue.record("expected base64_encode to plan successfully"); return
|
|
}
|
|
let body = renderCallable(plan)
|
|
#expect(body.contains("_withScalarArray("))
|
|
#expect(body.contains("data: [UInt8]"))
|
|
#expect(body.contains("numericCast(data.count)"))
|
|
}
|
|
}
|