// InterfaceSignalGenerationTests.swift // Covers Phase D4.2: GObject interface signals. Tier 1 (GLib + GObject) has // zero interface signals at runtime — every tier-1 signal lives on a class // (`Object`, `BindingGroup`) — so the compile gate and smoke tests cannot // exercise this path. This unit test is the only proof that // `renderInterface` actually emits a signal extension + trampoline instead // of silently dropping `InterfacePlan.signals` (the D4.2 regression this // suite guards against). import Testing @testable import GObjectGeneratorCore @Suite("Interface signal generation") struct InterfaceSignalGenerationTests { 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"), ] ) ]) let registry = TypeRegistry(repositories: ["GObject": gobject]) return MapContext(registry: registry, currentModule: "GObject", currentNamespace: "GObject") } /// Plans a one-off interface carrying `signals` and renders it, returning /// the interface file body plus the plan and member skips. func renderInterfaceFile(named name: String, signals: [Signal]) -> (source: String, plan: InterfacePlan, skips: [SkipEntry]) { let iface = Interface(name: name, cType: "G\(name)", signals: signals, getTypeFunction: "g_\(name.lowercased())_get_type") let (plan, skips) = planInterface(iface, context: makeContext()) let module = ModulePlan(module: "GObject", types: [.interface(plan)], skips: skips, coverage: CoverageStats()) return (renderModule(module)["\(name).swift"] ?? "", plan, skips) } @Test("An interface signal renders a protocol-extension connect method and a file-level trampoline") func interfaceSignalRendersTrampolineAndConnect() throws { let clicked = Signal(name: "clicked", isDetailed: false) let (source, plan, skips) = renderInterfaceFile(named: "Clickable", signals: [clicked]) #expect(skips.isEmpty) #expect(plan.signals.count == 1) // (a) The protocol declaration is unchanged — still just the // `pointer` requirement and any methods/properties, no signal noise // inside the protocol body itself. #expect(source.contains("public protocol Clickable {")) #expect(source.contains("var pointer: UnsafeMutableRawPointer { get }")) // (b) A protocol extension supplies the connect method as a default // implementation — signals are not protocol requirements (the C // signal-emission machinery is identical across all conformers). #expect(source.contains("extension Clickable {")) #expect(source.contains("func connectClicked(_ handler:")) // (c) A file-level @_cdecl nonisolated trampoline exists (same // pattern as class signals) — this is the D4.2 regression check: // the previous implementation planned interface signals but the // renderer silently dropped them. #expect(source.contains("@_cdecl(\"_trampolineGObjectClickableClicked\")")) #expect(source.contains("nonisolated func _trampolineGObjectClickableClicked(")) #expect(source.contains("MainActor.assumeIsolated")) } @Test("An interface with no signals renders no extension and no trampoline") func interfaceWithoutSignalsOmitsExtension() throws { let (source, plan, skips) = renderInterfaceFile(named: "Plain", signals: []) #expect(skips.isEmpty) #expect(plan.signals.isEmpty) #expect(!source.contains("extension Plain {")) #expect(!source.contains("@_cdecl")) } }