Destroy-notify ABI fixed to GClosureNotify's real 2-arg signature and wired into every connect method (was leaking every _ClosureBox, and UB on non-x86_64 with the wrong arg count). Trampolines restore MainActor isolation via assumeIsolated, with narrowly-scoped nonisolated(unsafe) shadow copies to satisfy Swift 6's sending checker. Interface-signal rendering implemented and unit-tested. Dead code removed (SignalHandlePlan), destroyTrampoline made non-optional, D7 deferral documented in-code. CoverageStats gained boundCallbacks/boundSignals counters. Added SignalGenerationTests, InterfaceSignalGenerationTests, and CallbackGenerationTests (12 new tests, 187/187 total). Fixed the dead nonDetailedSignal smoke test to actually mutate a property and assert the closure fired. Callback-param planner-side binding (D4.3) stays disabled: enabling it trips a genuine Swift compiler crash on g_qsort_with_data's GCompareDataFunc parameter. The renderer-side box setup/release logic is implemented and unit-tested by constructing plans directly, bypassing the blocked planner path. Verified: swift test (187/187), compile-gate.sh 1 --fresh (PASS), smoke-test.sh --fresh (18/18).
258 lines
11 KiB
Swift
258 lines
11 KiB
Swift
// SmokeTests.swift
|
|
// Runtime smoke tests for the GENERATED bindings.
|
|
//
|
|
// These are fundamentally different from the compile gate. The gate proves the
|
|
// generated Swift *type-checks*; these prove the underlying C functionality is
|
|
// actually reachable and correct *through the wrappers* — real GLib/GObject
|
|
// symbols, linked via pkg-config, invoked at runtime and their results checked.
|
|
//
|
|
// This file is not generated. `scripts/smoke-test.sh` generates the tier-1
|
|
// bindings with a SmokeTests target and copies this file in before running
|
|
// `swift test`.
|
|
|
|
import Testing
|
|
|
|
import GLib
|
|
import GObject
|
|
|
|
@Suite("Runtime smoke tests")
|
|
struct SmokeTests {
|
|
// MARK: - Free functions (string marshalling: withCString + String(cString:))
|
|
|
|
@Test("g_ascii_strup uppercases a string through the binding")
|
|
func asciiUppercase() {
|
|
#expect(asciiStrup(str: "hello, world", len: -1) == "HELLO, WORLD")
|
|
}
|
|
|
|
@Test("g_ascii_strdown lowercases a string through the binding")
|
|
func asciiLowercase() {
|
|
#expect(asciiStrdown(str: "HELLO, World", len: -1) == "hello, world")
|
|
}
|
|
|
|
// MARK: - Object construction + instance methods
|
|
|
|
@Test("A GObject subclass constructs and answers C queries")
|
|
func constructAndQuery() {
|
|
let group = BindingGroup()
|
|
// GBindingGroup is a plain GObject (not InitiallyUnowned), so a freshly
|
|
// constructed instance is not floating. This exercises, end to end:
|
|
// the C constructor, ownership adoption, the instance-pointer cast, and
|
|
// the gboolean → Bool return bridge.
|
|
#expect(group.isFloating() == false)
|
|
// Paired notify freeze/thaw must not trap.
|
|
group.freezeNotify()
|
|
group.thawNotify()
|
|
}
|
|
|
|
@Test("GObject user-data round-trips through set/get")
|
|
func userDataRoundtrip() {
|
|
let group = BindingGroup()
|
|
let marker = UnsafeMutableRawPointer(bitPattern: 0xBEEF)
|
|
group.setData(key: "smoke", data: marker)
|
|
#expect(group.getData(key: "smoke") == marker)
|
|
}
|
|
|
|
// MARK: - Boxed record memory management (C4: init(retaining:) + isolated deinit)
|
|
//
|
|
// These prove the copy/free functions the planner resolves are correct at
|
|
// runtime: a wrong copy (aliasing instead of duplicating) or a wrong free
|
|
// (double-free / freeing a borrowed pointer) corrupts the heap and aborts
|
|
// the process once enough alloc/free cycles run. The loops make such a bug
|
|
// deterministic rather than intermittent.
|
|
|
|
@Test("Signal roundtrip: notify::source fires on property change, disconnect prevents re-fire")
|
|
func notifySignalRoundtrip() {
|
|
let group = BindingGroup()
|
|
let obj = BindingGroup()
|
|
var fired = false
|
|
|
|
var handler = group.connectNotify(detail: "source") { _, _ in
|
|
fired = true
|
|
}
|
|
|
|
group.setSource(source: obj)
|
|
#expect(fired)
|
|
|
|
fired = false
|
|
handler.disconnect()
|
|
|
|
group.setSource(source: nil)
|
|
#expect(!fired)
|
|
}
|
|
|
|
@Test("Non-detailed signal connect: bare notify fires on property mutation")
|
|
func nonDetailedSignal() {
|
|
let group = BindingGroup()
|
|
var fired = false
|
|
var handler = group.connectNotify(detail: nil) { _, _ in
|
|
fired = true
|
|
}
|
|
let obj = BindingGroup()
|
|
group.setSource(source: obj) // property mutation fires bare "notify"
|
|
#expect(fired) // handler was invoked
|
|
fired = false
|
|
handler.disconnect()
|
|
group.setSource(source: nil)
|
|
#expect(!fired) // disconnected — handler not re-invoked
|
|
}
|
|
|
|
// MARK: - Out-parameters marshalled as tuple returns (C3)
|
|
//
|
|
// Every value the C function writes through a pointer must surface as a
|
|
// labelled tuple element with the right Swift type and value.
|
|
|
|
@Test("Multi out-param: g_unichar_compose composes and reports success")
|
|
func outParamsCompose() {
|
|
// U+0041 'A' + U+030A combining ring above → U+00C5 'Å'.
|
|
let (composed, ch) = unicharCompose(a: 0x0041, b: 0x030A)
|
|
#expect(composed == true)
|
|
#expect(ch == 0x00C5)
|
|
}
|
|
|
|
@Test("Multi out-param: g_unichar_decompose is the inverse of compose")
|
|
func outParamsDecompose() {
|
|
// U+00C5 'Å' → base U+0041 'A' and combining U+030A.
|
|
let (ok, a, b) = unicharDecompose(ch: 0x00C5)
|
|
#expect(ok == true)
|
|
#expect(a == 0x0041)
|
|
#expect(b == 0x030A)
|
|
}
|
|
|
|
@Test("Out-param string: g_ascii_strtoll returns value and unparsed tail")
|
|
func outParamEndptr() {
|
|
let (value, endptr) = asciiStrtoll(nptr: "123abc", base: 10)
|
|
#expect(value == 123)
|
|
#expect(endptr == "abc")
|
|
}
|
|
|
|
// MARK: - Enum returns (C-enum rawValue → Swift enum)
|
|
|
|
@Test("g_unichar_type returns the correct Unicode category enum")
|
|
func enumReturn() {
|
|
#expect(unicharType(c: 0x0041) == .uppercase_letter) // 'A'
|
|
#expect(unicharType(c: 0x0061) == .lowercase_letter) // 'a'
|
|
#expect(unicharType(c: 0x0031) == .decimal_number) // '1'
|
|
}
|
|
|
|
// MARK: - Bitfield (OptionSet) arguments reach C correctly
|
|
|
|
@Test("g_file_test distinguishes directory from regular file via FileTest bits")
|
|
func bitfieldArgument() {
|
|
#expect(fileTest(filename: "/", test: .is_dir) == true)
|
|
#expect(fileTest(filename: "/", test: .is_regular) == false)
|
|
}
|
|
|
|
// MARK: - GError bridging: both the success and the throwing path (C2)
|
|
|
|
@Test("Throwing function returns normally on success and throws on failure")
|
|
func throwsSuccessAndFailure() throws {
|
|
// In-range parse succeeds and yields the value through the out-param.
|
|
let (ok, num) = try asciiStringToUnsigned(str: "42", base: 10, min: 0, max: 100)
|
|
#expect(ok == true)
|
|
#expect(num == 42)
|
|
// Out-of-range parse sets a GError, which must surface as a Swift throw.
|
|
#expect(throws: (any Error).self) {
|
|
_ = try asciiStringToUnsigned(str: "999", base: 10, min: 0, max: 100)
|
|
}
|
|
}
|
|
|
|
@Test("g_spawn_check_wait_status: status 0 succeeds, non-zero throws")
|
|
func throwsOnNonZeroExit() throws {
|
|
#expect(try spawnCheckWaitStatus(waitStatus: 0) == true)
|
|
#expect(throws: (any Error).self) {
|
|
_ = try spawnCheckWaitStatus(waitStatus: 1 << 8) // exit code 1
|
|
}
|
|
}
|
|
|
|
// MARK: - GObject properties via the GValue machinery (C6)
|
|
//
|
|
// These exercise the generated computed-property accessors end to end:
|
|
// `g_value_init` → `g_object_set_property` / `g_object_get_property` →
|
|
// `g_value_set_*` / `g_value_get_*`, plus the bridging each type needs. A
|
|
// compile gate cannot catch a wrong GType macro, a mismatched value suffix,
|
|
// or a bad numeric bridge — only running against real GObject can.
|
|
|
|
@Test("An object property round-trips through the GValue getter and setter")
|
|
func objectPropertyRoundtrip() {
|
|
// GBindingGroup.source is a writable GObject-typed property: setting it
|
|
// goes through g_value_set_object + g_object_set_property, and reading it
|
|
// back through g_object_get_property + g_value_get_object + init(retaining:).
|
|
let group = BindingGroup()
|
|
let source = BindingGroup()
|
|
group.source = source
|
|
// The getter returns a fresh wrapper around the same underlying GObject.
|
|
#expect(group.source.pointer == source.pointer)
|
|
}
|
|
|
|
@Test("Delegated property accessors forward to their getter= methods")
|
|
func delegatedPropertyAccessors() {
|
|
// GBinding.flags/source carry GIR getter= attributes, so their property
|
|
// accessors delegate to getFlags()/getSource() rather than going through
|
|
// GValue. This exercises that delegation end to end — and the nullability
|
|
// it inherits: GBinding.source is `Object?` (get_source is nullable),
|
|
// where the uniform GValue path would have produced a trap-prone `Object`.
|
|
let a = BindingGroup()
|
|
let b = BindingGroup()
|
|
let binding = a.bindProperty(sourceProperty: "source", target: b,
|
|
targetProperty: "source", flags: .bidirectional)
|
|
// flags → getFlags() (numericCast(guint) bridge inside the method).
|
|
#expect(binding.flags.contains(.bidirectional))
|
|
// source → getSource() returning Object?; identity matches the binding's source.
|
|
#expect(binding.source?.pointer == a.pointer)
|
|
}
|
|
// MARK: - Additional coverage (added per Phase-C review)
|
|
//
|
|
// Scenarios delivered:
|
|
// • Static-function call returning a non-trivial value (`getUserName`).
|
|
// • Throwing GError surface — domain (GQuark), code, and message values.
|
|
// • Pointer-return throwing function with `result!` unwrap (`uriParse`
|
|
// returns a boxed `Uri` through `Uri(takingOwnership: _rawPointer(result!))`).
|
|
//
|
|
// Scenarios omitted (per plan's Step-8 contingency — record which symbol
|
|
// was considered and why no tier-1 symbol fits):
|
|
// • GValue string/enum writable-property round-trip: NO tier-1 symbol
|
|
// fits. Bound writable GValue-path properties are all G_TYPE_OBJECT
|
|
// (`BindingGroup.source`, `SignalGroup.target`); no string-typed or
|
|
// enum-typed writable property exists in tier-1. The string-path
|
|
// `g_value_unset` setter fix (Step 2) is therefore proven by the unit
|
|
// test `setterEmitsGValueUnset`, not end-to-end here. The object-path
|
|
// setter `g_value_unset` IS exercised end-to-end by
|
|
// `objectPropertyRoundtrip` above.
|
|
|
|
@Test("g_get_user_name returns a non-empty string through a static function")
|
|
func staticFunction() {
|
|
let name = getUserName()
|
|
#expect(!name.isEmpty)
|
|
}
|
|
|
|
@Test("Throwing GError supplies domain, code, and message")
|
|
func throwingErrorValues() throws {
|
|
#expect(throws: (any Error).self) {
|
|
do {
|
|
_ = try spawnCheckWaitStatus(waitStatus: 1 << 8) // exit code 1
|
|
} catch let error as GLibError {
|
|
// domain is a GQuark (UInt32) — verify it's non-zero
|
|
#expect(error.domain != 0)
|
|
// code is the exit status: (waitStatus >> 8) & 0xff = 256 >> 8 = 1
|
|
#expect(error.code == 1)
|
|
#expect(!error.message.isEmpty)
|
|
throw error // re-throw to satisfy #expect(throws:)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test("Throwing function: fileReadLink resolves /etc/localtime and throws on nonexistent path")
|
|
func pointerReturnThrowingFunction() throws {
|
|
// fileReadLink returns a String (or throws GLibError). This tests the
|
|
// throwing function body path with a string return.
|
|
let link = try fileReadLink(filename: "/etc/localtime")
|
|
#expect(!link.isEmpty)
|
|
|
|
do {
|
|
_ = try fileReadLink(filename: "/nonexistent/path/that/does/not/exist")
|
|
Issue.record("expected fileReadLink to throw")
|
|
} catch {
|
|
#expect((error as? GLibError) != nil)
|
|
}
|
|
}
|
|
}
|