// 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 @_spi(SGTKInternal) 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) } } }