// 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")) } @Test("transfer-full boxed params copy the value before the call") func transferFullBoxedParamCopiesValue() throws { let boxed = Record( name: "Box", cType: "GBox", getTypeFunction: "g_box_get_type", methods: [ Method(name: "copy", cIdentifier: "g_box_copy", parameters: [Parameter(name: "self", type: .pointer, isInstanceParameter: true)], returnValue: ReturnValue(type: .typeRef("Box", namespace: "GObject"))), releaseMethodForTest("free", "g_box_free"), ]) let voidBoxed = Record( name: "VoidBox", cType: "GVoidBox", getTypeFunction: "g_void_box_get_type", methods: [ releaseMethodForTest("ref", "g_void_box_ref"), releaseMethodForTest("unref", "g_void_box_unref"), ]) let repository = Repository(namespaces: [ Namespace(name: "GObject", version: "2.0", records: [boxed, voidBoxed]) ]) let registry = TypeRegistry(repositories: ["GObject": repository]) let context = MapContext(registry: registry, currentModule: "GObject", currentNamespace: "GObject") let copied = GlobalFunction( name: "take_box", cIdentifier: "g_take_box", parameters: [Parameter(name: "box", type: .typeRef("Box", namespace: "GObject"), transferOwnership: .full)], returnValue: ReturnValue(type: .void)) guard case .success(let copiedPlan) = planFunction(copied, context: context) else { Issue.record("expected transfer-full boxed parameter to plan") return } let copiedSource = renderCallable(copiedPlan) #expect(copiedSource.contains("g_box_copy(_instancePointer(box.pointer))")) #expect(copiedSource.contains("_rawPointer(g_box_copy(")) let consumed = GlobalFunction( name: "take_void_box", cIdentifier: "g_take_void_box", parameters: [Parameter(name: "box", type: .typeRef("VoidBox", namespace: "GObject"), transferOwnership: .full)], returnValue: ReturnValue(type: .void)) guard case .success(let consumedPlan) = planFunction(consumed, context: context) else { Issue.record("expected void-returning boxed ref parameter to plan") return } let consumedSource = renderCallable(consumedPlan) #expect(consumedSource.contains("g_void_box_ref(pointer)")) #expect(!consumedSource.contains("_rawPointer(g_void_box_ref(")) } /// Creates a parameterless record lifetime method for renderer fixtures. private func releaseMethodForTest(_ name: String, _ cIdentifier: String) -> Method { Method(name: name, cIdentifier: cIdentifier, parameters: [Parameter(name: "self", type: .pointer, isInstanceParameter: true)]) } // 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?")) 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?")) #expect(!renderCallable(constPlan).contains("UnsafeMutablePointer?")) } @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("Colliding constructor signatures are disambiguated by constructor name") 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 == 2) #expect(plan.constructors[0].cIdentifier == "g_connection_new_finish") #expect(plan.constructors[1].cIdentifier == "g_connection_new_for_address_finish") #expect(plan.constructors[1].parameters[0].swiftName == "forAddressFinishRes") let module = ModulePlan(module: "GObject", types: [.class(plan)], skips: [], coverage: CoverageStats()) let src = renderModule(module)["Connection.swift"] ?? "" #expect(src.contains("convenience init(res: AsyncResult) throws")) #expect(src.contains("convenience init(forAddressFinishRes: AsyncResult) throws")) } // 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)")) } }