1
0
Fork 0
gobject-generator/smoke/SmokeTests.swift
Brendan Szymanski 16e5449cad Expand runtime smoke tests across binding categories
Add smoke tests exercising boxed-record lifetime (copy-on-borrow,
adopt-on-full, init(retaining:), isolated deinit over thousands of alloc/free
cycles), out-param tuple returns, enum returns, bitfield arguments, and GError
bridging on both the success and throwing paths — proving each marshalling
category reaches real GLib/GObject correctly at runtime.
2026-07-17 21:04:43 -04:00

168 lines
6.9 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("Boxed record copies a borrowed return and frees it on deinit")
func boxedBorrowedReturnRoundtrips() {
// g_variant_type_checked_ returns a borrowed pointer; the wrapper copies
// it (g_variant_type_copy) and frees the copy (g_variant_type_free) on
// deinit. Freeing the borrowed original instead would corrupt GLib's
// internal type table.
for _ in 0..<5000 {
let t = variantTypeChecked(typeString: "(sias)")
#expect(UInt(bitPattern: t.pointer) != 0)
}
}
@Test("Boxed record adopts a full-transfer return and frees it on deinit")
func boxedFullTransferReturnRoundtrips() throws {
// g_uri_parse returns transfer-full GUri*; the wrapper adopts it and
// frees with g_uri_unref on deinit. A missing unref leaks; a double
// unref aborts.
for _ in 0..<5000 {
let uri = try uriParse(uriString: "https://example.com/a/b?q=1#frag", flags: [])
#expect(UInt(bitPattern: uri.pointer) != 0)
}
}
@Test("init(retaining:) makes an independently-owned copy")
func boxedRetainingInitIndependentCopy() {
// Copy one base pointer many times through init(retaining:), then free
// every copy. If the copy aliased the base (no real duplication) the
// frees would destroy the base's storage; the final base access would
// then be a use-after-free.
let base = variantTypeChecked(typeString: "as")
var copies: [VariantType] = []
for _ in 0..<2000 {
copies.append(VariantType(retaining: base.pointer))
}
copies.removeAll() // 2000 independent frees
#expect(UInt(bitPattern: base.pointer) != 0) // base survives
}
// 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
}
}
}