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.
168 lines
6.9 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|