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

152 lines
7.1 KiB
Swift

// RecordGenerationTests.swift
// Covers Phase C4: boxed-record memory management. A boxed wrapper owns its
// pointer, so it needs a `deinit` that frees it and an `init(retaining:)` that
// copies a borrowed pointer. The free/copy functions are resolved from the GIR
// `free-function`/`copy-function` attributes when present, else from the
// record's own `unref`/`free` and `ref`/`copy` methods. These tests lock in
// that resolution and the rendered surface (`isolated deinit`, the copy init).
import Testing
@testable import GObjectGeneratorCore
@Suite("Record generation")
struct RecordGenerationTests {
func makeContext() -> MapContext {
let glib = Repository(namespaces: [Namespace(name: "GLib", version: "2.0")])
let registry = TypeRegistry(repositories: ["GLib": glib])
return MapContext(registry: registry, currentModule: "GLib", currentNamespace: "GLib")
}
/// Renders a single planned record to Swift source via the module renderer.
func render(_ plan: RecordPlan) -> String {
let module = ModulePlan(module: "GLib", types: [.record(plan)], skips: [], coverage: CoverageStats())
return renderModule(module)["\(plan.name).swift"] ?? ""
}
/// A record method taking only its instance parameter (a canonical
/// `ref`/`unref`/`copy`/`free` shape).
func releaseMethod(_ name: String, _ cIdentifier: String) -> Method {
Method(name: name, cIdentifier: cIdentifier,
parameters: [Parameter(name: "self", type: .pointer, isInstanceParameter: true)])
}
@Test("Explicit free/copy attributes take priority over method scan")
func explicitAttributesWin() {
let record = Record(
name: "Bytes", cType: "GBytes", getTypeFunction: "g_bytes_get_type",
copyFunction: "g_bytes_ref", freeFunction: "g_bytes_unref",
methods: [releaseMethod("ref", "g_bytes_ref_method"),
releaseMethod("unref", "g_bytes_unref_method")]
)
#expect(resolvedFreeFunction(record) == "g_bytes_unref")
#expect(resolvedCopyFunction(record) == "g_bytes_ref")
}
@Test("Falls back to the record's own unref/ref methods")
func methodScanFallback() {
// No explicit attributes resolve from methods. `unref`/`ref` are
// preferred over `free`/`copy` when both exist.
let record = Record(
name: "TimeZone", cType: "GTimeZone", getTypeFunction: "g_time_zone_get_type",
methods: [releaseMethod("copy", "g_time_zone_copy"),
releaseMethod("free", "g_time_zone_free"),
releaseMethod("ref", "g_time_zone_ref"),
releaseMethod("unref", "g_time_zone_unref")]
)
#expect(resolvedFreeFunction(record) == "g_time_zone_unref")
#expect(resolvedCopyFunction(record) == "g_time_zone_ref")
}
@Test("A method taking extra arguments is not treated as a destructor")
func destructorMustBeParameterless() {
let record = Record(
name: "Weird", cType: "GWeird", getTypeFunction: "g_weird_get_type",
methods: [Method(name: "free", cIdentifier: "g_weird_free",
parameters: [Parameter(name: "self", type: .pointer, isInstanceParameter: true),
Parameter(name: "flags", type: .int32)])]
)
#expect(resolvedFreeFunction(record) == nil)
}
@Test("Boxed record renders init(retaining:) and an isolated deinit")
func rendersMemoryManagement() {
let plan = RecordPlan(
name: "VariantType", cType: "GVariantType",
getTypeFunction: "g_variant_type_get_gtype",
copyFunction: "g_variant_type_copy", freeFunction: "g_variant_type_free"
)
let source = render(plan)
#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))"))
// Records with a free function claim to take responsibility for freeing.
#expect(source.contains("takes responsibility"))
#expect(!source.contains("never frees"))
}
@Test("A record with no resolvable free renders no deinit")
func noFreeNoDeinit() {
let plan = RecordPlan(name: "PollFD", cType: "GPollFD")
let source = render(plan)
#expect(!source.contains("deinit"))
#expect(!source.contains("retaining"))
// The takingOwnership init is always present.
#expect(source.contains("takingOwnership"))
// No-free records must not claim to free the pointee.
#expect(source.contains("never frees"))
#expect(!source.contains("takes responsibility"))
}
// MARK: - C4 copy/free pairing
@Test("copy+unref without free+ref is rejected as crossed pairing")
func crossedCopyUnrefPairing() {
// Record has `copy` but `unref` (no `free` or `ref`).
// The old independent-scan would pair copy with unref (wrong).
// The new paired resolver should return (nil, nil) because the categories
// are inconsistent copy+free (copy semantics) nor ref+unref (refcount)
// neither is fully present.
let record = Record(
name: "WeirdBuf", cType: "GWeirdBuf", getTypeFunction: "g_weird_buf_get_type",
methods: [
releaseMethod("copy", "g_weird_buf_copy"),
releaseMethod("unref", "g_weird_buf_unref"),
]
)
#expect(resolvedFreeFunction(record) == nil)
#expect(resolvedCopyFunction(record) == nil)
}
@Test("copy+free together are paired correctly")
func pairedCopyFree() {
let record = Record(
name: "CopyFree", cType: "GCopyFree", getTypeFunction: "g_copy_free_get_type",
methods: [
releaseMethod("copy", "g_copy_free_copy"),
releaseMethod("free", "g_copy_free_free"),
]
)
#expect(resolvedFreeFunction(record) == "g_copy_free_free")
#expect(resolvedCopyFunction(record) == "g_copy_free_copy")
}
@Test("ref+unref together (refcount) take priority over copy+free")
func refcountTakesPriority() {
let record = Record(
name: "Priority", cType: "GPriority", getTypeFunction: "g_priority_get_type",
methods: [
releaseMethod("ref", "g_priority_ref"),
releaseMethod("unref", "g_priority_unref"),
releaseMethod("copy", "g_priority_copy"),
releaseMethod("free", "g_priority_free"),
]
)
// refcount pair wins over copy/free.
#expect(resolvedFreeFunction(record) == "g_priority_unref")
#expect(resolvedCopyFunction(record) == "g_priority_ref")
}
}