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:
parent
dac435ec89
commit
c20015be48
11 changed files with 180 additions and 12 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))"))
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue