1
0
Fork 0
gobject-generator/smoke/SmokeTests.swift
Brendan Szymanski 5abb48e889 Fix Phase D signal/callback remediation defects
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).
2026-07-18 20:45:56 -04:00

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)
}
}
}