1
0
Fork 0
gobject-generator/Tests/SwiftGtkGenCoreTests/FunctionGenerationTests.swift

171 lines
7.2 KiB
Swift

// FunctionGenerationTests.swift
// Covers Phase C1: namespace-level function planning and rendering the
// C-type bridging that lets primitive, enum, bitfield, and const-string
// callables generate compilable Swift. These lock in the boundary decisions
// (numericCast for width-ambiguous integers, withCString for input strings,
// and the skips for varargs / mutable buffers / string vectors) so a
// regression surfaces here rather than as a mysterious drop in gate coverage.
import Testing
@testable import SwiftGtkGenCore
@Suite("Function generation")
struct FunctionGenerationTests {
/// A registry-backed context in the GLib module. No named types are needed:
/// these tests exercise primitives, strings, and bitfields only.
func makeContext() -> MapContext {
let glib = Repository(namespaces: [
Namespace(
name: "GLib", version: "2.0",
bitfields: [
Bitfield(name: "LogLevelFlags", cType: "GLogLevelFlags",
getTypeFunction: "g_log_level_flags_get_type")
]
)
])
let registry = TypeRegistry(repositories: ["GLib": glib])
return MapContext(registry: registry, currentModule: "GLib", currentNamespace: "GLib")
}
/// Renders a single planned callable to Swift source via the public module
/// renderer, returning the merged `Functions.swift` body it lands in.
func render(_ plan: CallablePlan) -> String {
let module = ModulePlan(
module: "GLib",
types: [.callable(plan)],
skips: [],
coverage: CoverageStats()
)
let files = renderModule(module)
return files["Functions.swift"] ?? ""
}
// MARK: - Plannable callables
@Test("Width-ambiguous integer parameters bridge through numericCast")
func integerParameterBridging() throws {
let fn = GlobalFunction(
name: "free_sized", cIdentifier: "g_free_sized",
parameters: [
Parameter(name: "mem", type: .pointer, cType: "gpointer"),
Parameter(name: "size", type: .size, cType: "gsize"),
]
)
guard case .success(let plan) = planFunction(fn, context: makeContext()) else {
Issue.record("expected g_free_sized to plan successfully"); return
}
#expect(plan.parameters.count == 2)
#expect(plan.parameters[1].mapping.marshalIn == .numericCast(targetType: "UInt"))
let body = render(plan)
#expect(body.contains("g_free_sized(mem, numericCast(size))"))
}
@Test("Const string parameters are bridged with withCString")
func stringParameterBridging() throws {
let fn = GlobalFunction(
name: "set_application_name", cIdentifier: "g_set_application_name",
parameters: [
Parameter(name: "application_name", type: .string, cType: "const char*")
]
)
guard case .success(let plan) = planFunction(fn, context: makeContext()) else {
Issue.record("expected const-string function to plan successfully"); return
}
#expect(plan.parameters[0].mapping.marshalIn == .stringToC)
let body = render(plan)
#expect(body.contains("func setApplicationName(applicationName: String)"))
#expect(body.contains("applicationName.withCString { cString0 in"))
#expect(body.contains("g_set_application_name(cString0)"))
}
@Test("Bitfield parameters and returns round-trip through the C flags type")
func bitfieldRoundTrip() throws {
let fn = GlobalFunction(
name: "log_set_always_fatal", cIdentifier: "g_log_set_always_fatal",
parameters: [
Parameter(name: "fatal_mask", type: .typeRef("LogLevelFlags"), cType: "GLogLevelFlags")
],
returnValue: ReturnValue(type: .typeRef("LogLevelFlags"))
)
guard case .success(let plan) = planFunction(fn, context: makeContext()) else {
Issue.record("expected bitfield function to plan successfully"); return
}
let body = render(plan)
#expect(body.contains("GLogLevelFlags(rawValue: numericCast(fatalMask.rawValue))"))
#expect(body.contains("LogLevelFlags(rawValue: numericCast("))
}
// MARK: - Skips
@Test("Variadic functions are skipped with the varargs reason")
func variadicSkip() throws {
let fn = GlobalFunction(
name: "build_filename", cIdentifier: "g_build_filename",
parameters: [
Parameter(name: "first_element", type: .string, cType: "const char*"),
Parameter(name: "...", type: .void),
]
)
guard case .skip(let entry) = planFunction(fn, context: makeContext()) else {
Issue.record("expected variadic function to be skipped"); return
}
#expect(entry.reason == .varargs)
}
@Test("Mutable string buffers and string vectors are skipped")
func mutableAndVectorStringSkips() throws {
let mutableBuffer = GlobalFunction(
name: "utf8_strncpy", cIdentifier: "g_utf8_strncpy",
parameters: [Parameter(name: "dest", type: .string, cType: "char*")]
)
guard case .skip = planFunction(mutableBuffer, context: makeContext()) else {
Issue.record("expected mutable string buffer to be skipped"); return
}
let stringVector = GlobalFunction(
name: "cmp_strv", cIdentifier: "g_cmp_strv",
parameters: [Parameter(name: "arg1", type: .string, cType: "const char* const*")]
)
guard case .skip = planFunction(stringVector, context: makeContext()) else {
Issue.record("expected string vector to be skipped"); return
}
}
@Test("Unexported C symbols never reach a plan")
func missingSymbolSkip() throws {
// g_open is a `#define g_open open` macro on non-Windows: not a real
// symbol. The planner must skip it before rendering an uncompilable call.
var skips: [SkipEntry] = []
var types: [TypePlan] = []
let fn = GlobalFunction(name: "open", cIdentifier: "g_open")
let bound = planOrSkipFunctionForTest(&skips, &types, fn)
#expect(bound == 0)
#expect(types.isEmpty)
#expect(skips.count == 1)
}
/// Test shim over the private `planOrSkipFunction`, exercised through the
/// public planner so the missing-symbol gate is covered end to end.
private func planOrSkipFunctionForTest(
_ skips: inout [SkipEntry], _ types: inout [TypePlan], _ fn: GlobalFunction
) -> Int {
let ns = Namespace(name: "GLib", version: "2.0", functions: [fn])
let repo = Repository(namespaces: [ns])
let analysis = MultiPackageAnalysis(
repositories: ["GLib": repo],
directDependencies: ["GLib": []],
transitiveDependencies: ["GLib": []],
implicitImports: ["GLib": []],
packageConfigs: [:]
)
let registry = TypeRegistry(repositories: ["GLib": repo])
let plans = planModules(analysis: analysis, registry: registry)
let module = plans["GLib"]
skips = module?.skips ?? []
types = module?.types ?? []
return module?.coverage.boundCallables ?? 0
}
}