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.
This commit is contained in:
parent
164f8b7b9a
commit
16e5449cad
1 changed files with 114 additions and 0 deletions
|
|
@ -51,4 +51,118 @@ struct SmokeTests {
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue