171 lines
7.2 KiB
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
|
|
}
|
|
}
|