1
0
Fork 0
gobject-generator/Tests/GObjectGeneratorCoreTests/InterfaceSignalGenerationTests.swift

78 lines
3.9 KiB
Swift

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