152 lines
7.1 KiB
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")
|
|
}
|
|
}
|