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

213 lines
11 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")
}
@Test("Boxed record renders callables but hides lifetime methods")
func rendersRecordCallablesWithoutLifetimeMethods() {
let record = Record(
name: "BreakpointCondition", cType: "AdwBreakpointCondition",
getTypeFunction: "adw_breakpoint_condition_get_type",
copyFunction: "adw_breakpoint_condition_copy",
freeFunction: "adw_breakpoint_condition_free",
methods: [
releaseMethod("free", "adw_breakpoint_condition_free"),
releaseMethod("unref", "adw_breakpoint_condition_unref"),
Method(name: "to_string", cIdentifier: "adw_breakpoint_condition_to_string",
parameters: [Parameter(name: "self", type: .pointer, isInstanceParameter: true)],
returnValue: ReturnValue(type: .string)),
],
constructors: [
Constructor(name: "new_length", cIdentifier: "adw_breakpoint_condition_new_length",
parameters: [Parameter(name: "value", type: .double)]),
Constructor(name: "new_and", cIdentifier: "adw_breakpoint_condition_new_and",
parameters: [Parameter(name: "condition1", type: .double),
Parameter(name: "condition2", type: .double)]),
Constructor(name: "new_or", cIdentifier: "adw_breakpoint_condition_new_or",
parameters: [Parameter(name: "condition1", type: .double),
Parameter(name: "condition2", type: .double)]),
],
functions: [GlobalFunction(name: "parse", cIdentifier: "adw_breakpoint_condition_parse",
parameters: [Parameter(name: "value", type: .string, cType: "const char*")])]
)
let result = planRecord(record, context: makeContext())
let source = render(result.plan)
#expect(result.memberSkips.isEmpty)
#expect(source.contains("public convenience init("))
#expect(source.contains("_rawPointer(adw_breakpoint_condition_new_length("))
#expect(source.contains("condition1: Double, condition2: Double"))
#expect(source.contains("orCondition1: Double, orCondition2: Double"))
#expect(source.contains("public func toString("))
#expect(source.contains("public static func parse("))
#expect(!source.contains("public func free("))
#expect(!source.contains("public func unref("))
}
@Test("moved-to namespace functions render on their record")
func routesMovedToFunction() {
let record = Record(name: "MyRecord", cType: "MyRecord",
getTypeFunction: "my_record_get_type")
let moved = GlobalFunction(
name: "my_record_do_thing", cIdentifier: "my_record_do_thing",
returnValue: ReturnValue(type: .int32),
symbolInfo: SymbolInfo(movedTo: "MyRecord.do_thing"))
let namespace = Namespace(name: "Test", version: "1.0",
records: [record], functions: [moved])
let repository = Repository(namespaces: [namespace])
let analysis = MultiPackageAnalysis(
repositories: ["Test": repository],
directDependencies: ["Test": []], transitiveDependencies: ["Test": []],
implicitImports: ["Test": []], packageConfigs: [:])
let registry = TypeRegistry(repositories: ["Test": repository])
let module = planModules(analysis: analysis, registry: registry)["Test"]!
let source = renderModule(module).values.joined(separator: "\n")
#expect(source.contains("public static func doThing("))
#expect(!source.contains("public func myRecordDoThing("))
}
}