78 lines
3.9 KiB
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"))
|
|
}
|
|
}
|