1
0
Fork 0

Complete Phase E4 tier-5 Gtk pointer-hiding and smoke coverage

Hides pointer storage/init behind @_spi(SGTKInternal) across boxed records
and interface Ref wrappers, marks interface Ref classes @MainActor,
threads boxed-record hasCopyFunction through GValue ops, and qualifies
re-exported dependency imports @_spi(SGTKInternal). Adds the tier-5
GtkSmoke display-free runtime smoke suite.
This commit is contained in:
Brendan Szymanski 2026-07-20 21:58:51 -04:00
parent dac435ec89
commit c20015be48
11 changed files with 180 additions and 12 deletions

View file

@ -42,8 +42,26 @@ struct CallbackGenerationTests {
coverage: CoverageStats())
let files = renderModule(module)
let source = files["Callbacks.swift"] ?? ""
#expect(source.contains("public typealias CompareFunc = @convention(c) (UnsafeRawPointer?, UnsafeRawPointer?) -> Int32"))
#expect(source.contains("public typealias CompareFuncSwift = (UnsafeRawPointer?, UnsafeRawPointer?) -> Int32"))
#expect(source.contains("@_spi(SGTKInternal) public typealias CompareFunc = @convention(c) (UnsafeRawPointer?, UnsafeRawPointer?) -> Int32"))
// The Swift-closure form still carries raw UnsafeRawPointer args here,
// so it is hidden behind @_spi too (Phase E4 gpointer policy).
#expect(source.contains("@_spi(SGTKInternal) public typealias CompareFuncSwift = (UnsafeRawPointer?, UnsafeRawPointer?) -> Int32"))
}
@Test("A pointer-free Swift-closure form stays plain public; only the @convention(c) form is @_spi")
func callbackTypePointerFreeSwiftFormStaysPublic() throws {
let plan = CallbackTypePlan(
name: "NotifyFunc",
swiftType: "(String) -> Void",
cSwiftType: "@convention(c) (UnsafeMutableRawPointer?) -> Void"
)
let module = ModulePlan(module: "GLib", types: [.callback(plan)], skips: [],
coverage: CoverageStats())
let files = renderModule(module)
let source = files["Callbacks.swift"] ?? ""
#expect(source.contains("@_spi(SGTKInternal) public typealias NotifyFunc = @convention(c) (UnsafeMutableRawPointer?) -> Void"))
#expect(source.contains("public typealias NotifyFuncSwift = (String) -> Void"))
#expect(!source.contains("@_spi(SGTKInternal) public typealias NotifyFuncSwift"))
}
// MARK: - Planner baseline (unchanged pending D4.3)

View file

@ -80,7 +80,7 @@ struct InterfaceConformanceTests {
// Protocol body is bare only the `pointer` requirement.
#expect(ifaceSrc.contains("public protocol TypePlugin {"))
#expect(ifaceSrc.contains("var pointer: UnsafeMutableRawPointer { get }"))
#expect(ifaceSrc.contains("@_spi(SGTKInternal) var pointer: UnsafeMutableRawPointer { get }"))
// The interface method is a protocol EXTENSION default, not a
// requirement it keeps its own (Void) signature regardless of the
@ -89,9 +89,10 @@ struct InterfaceConformanceTests {
#expect(ifaceSrc.contains("public func use("))
// Concrete Ref wrapper is emitted so `any TypePlugin` is constructible.
#expect(ifaceSrc.contains("public final class TypePluginRef: TypePlugin {"))
#expect(ifaceSrc.contains("init(retaining pointer: UnsafeMutableRawPointer)"))
#expect(ifaceSrc.contains("init(takingOwnership pointer: UnsafeMutableRawPointer)"))
#expect(ifaceSrc.contains("public final class TypePluginRef: @MainActor TypePlugin {"))
#expect(ifaceSrc.contains("@_spi(SGTKInternal) public let pointer: UnsafeMutableRawPointer"))
#expect(ifaceSrc.contains("@_spi(SGTKInternal) public init(retaining pointer: UnsafeMutableRawPointer)"))
#expect(ifaceSrc.contains("@_spi(SGTKInternal) public init(takingOwnership pointer: UnsafeMutableRawPointer)"))
#expect(ifaceSrc.contains("isolated deinit"))
}

View file

@ -115,6 +115,20 @@ struct PropertyGenerationTests {
#expect(!source.contains("g_object_set_property"))
}
// MARK: - Phase E4: no-unsafe-pointer public API policy
@Test("A root class's pointer storage and both instance inits are @_spi(SGTKInternal), the class itself stays public")
func classPointerSurfaceIsSPIGated() throws {
let prop = Property(name: "length", type: .int32, isReadable: true, isWritable: false)
let (source, _, _) = renderClass(named: "Buffer", properties: [prop])
#expect(source.contains("public class Buffer {"))
#expect(source.contains("@_spi(SGTKInternal) public let pointer: UnsafeMutableRawPointer"))
#expect(source.contains("@_spi(SGTKInternal) public required init(takingOwnership pointer: UnsafeMutableRawPointer)"))
#expect(source.contains("@_spi(SGTKInternal) public init(retaining pointer: UnsafeMutableRawPointer)"))
// The plain-public getter is unaffected Int32 carries no raw pointer.
#expect(source.contains(" public var length: Int32"))
}
@Test("Writable string property generates both a getter and a setter")
func writableStringProperty() throws {
let prop = Property(name: "label", type: .string, isReadable: true, isWritable: true)

View file

@ -77,7 +77,9 @@ struct RecordGenerationTests {
copyFunction: "g_variant_type_copy", freeFunction: "g_variant_type_free"
)
let source = render(plan)
#expect(source.contains("public init(retaining pointer: UnsafeMutableRawPointer)"))
#expect(source.contains("@_spi(SGTKInternal) public let pointer: UnsafeMutableRawPointer"))
#expect(source.contains("@_spi(SGTKInternal) public init(takingOwnership pointer: UnsafeMutableRawPointer)"))
#expect(source.contains("@_spi(SGTKInternal) public init(retaining pointer: UnsafeMutableRawPointer)"))
#expect(source.contains("g_variant_type_copy(_instancePointer(pointer))"))
#expect(source.contains("isolated deinit {"))
#expect(source.contains("g_variant_type_free(_instancePointer(pointer))"))

View file

@ -57,6 +57,37 @@ struct RendererCallableTests {
#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")

View file

@ -130,6 +130,40 @@ struct SignalGenerationTests {
#expect(source.contains("_sgtk_signal_connect_data("))
#expect(!source.contains("_sgtk_signal_connect_data(ptr, cName, unsafeBitCast(\\(plan.trampolineCName)"))
}
@Test("Support.swift hides GLibError.init(consuming:), SignalHandle.instance/init, and _sgtk_* helpers behind @_spi(SGTKInternal)")
func supportPointerSurfaceIsSPIGated() throws {
let notify = Signal(name: "notify", isDetailed: true)
let klass = Class(name: "Object", cType: "GObject", parent: nil,
getTypeFunction: "g_object_get_type", signals: [notify])
let (plan, skips) = planClass(klass, context: makeContext())
#expect(skips.isEmpty)
let module = ModulePlan(module: "GObject", dependencyModules: ["GLib"],
types: [.class(plan)], skips: [], coverage: CoverageStats())
let support = renderModule(module)["Support.swift"] ?? ""
#expect(support.contains("@_spi(SGTKInternal) public let instance: UnsafeMutableRawPointer"))
#expect(support.contains("@_spi(SGTKInternal) public init(id: UInt, instance: UnsafeMutableRawPointer)"))
#expect(support.contains("@_spi(SGTKInternal) public nonisolated func _sgtk_destroy_notify_impl("))
#expect(support.contains("@_spi(SGTKInternal) public nonisolated func _sgtk_signal_connect_data("))
#expect(support.contains("@_spi(SGTKInternal) public nonisolated func _sgtk_signal_handler_disconnect("))
// SignalHandle itself and its id/disconnect() stay plain public only
// the raw-pointer members are hidden.
#expect(support.contains("public struct SignalHandle {"))
#expect(support.contains("public let id: UInt"))
#expect(support.contains("public mutating func disconnect()"))
// Dependency import chokepoint is SPI too.
#expect(support.contains("@_spi(SGTKInternal) import GLib"))
}
@Test("Support.swift hides GLibError.init(consuming:) behind @_spi(SGTKInternal), struct/fields stay plain public")
func glibErrorConsumingInitIsSPIGated() throws {
let module = ModulePlan(module: "GLib", types: [], skips: [], coverage: CoverageStats())
let support = renderModule(module)["Support.swift"] ?? ""
#expect(support.contains("public struct GLibError: Swift.Error {"))
#expect(support.contains("public let domain: UInt32"))
#expect(support.contains("@_spi(SGTKInternal) public init(consuming error: UnsafeMutablePointer<GError>)"))
}
@Test("Boxed signal param wraps via retaining: when a copy function exists, takingOwnership: when it doesn't")
func boxedSignalParamWrapperSelection() throws {
let withCopy = Signal(