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