1
0
Fork 0
gobject-generator/Tests/GObjectGeneratorCoreTests/ParserSemanticsTests.swift

471 lines
22 KiB
Swift

// ParserSemanticsTests.swift
// Covers the GIR attributes the binding planner depends on: parameter
// direction, GError throwing, introspectability, GType registration, record
// type-structs, array length metadata, container element types, and record
// field types. Each of these was previously dropped on the floor by the parser.
import Foundation
import Testing
@testable import GObjectGeneratorCore
/// Wraps GIR element markup in a minimal well-formed repository document.
///
/// - Parameter body: The XML to place inside the `<namespace>` element.
/// - Returns: A complete GIR document string ready to parse.
private func girDocument(_ body: String) -> String {
"""
<?xml version="1.0"?>
<repository version="1.2"
xmlns="http://www.gtk.org/introspection/core/1.0"
xmlns:c="http://www.gtk.org/introspection/c/1.0"
xmlns:glib="http://www.gtk.org/introspection/glib/1.0">
<namespace name="Test" version="1.0" c:identifier-prefixes="Test">
\(body)
</namespace>
</repository>
"""
}
@Suite("Parser semantics")
struct ParserSemanticsTests {
/// Record fields previously always parsed as `.void` because the `<type>`
/// handler never routed to the enclosing field.
@Test("Record field types are populated")
func fieldTypesArePopulated() throws {
let repo = try GIRParser().parse(
xmlString: girDocument(
"""
<record name="RGBA" c:type="GdkRGBA" glib:get-type="gdk_rgba_get_type">
<field name="red" writable="1">
<type name="gfloat" c:type="float"/>
</field>
<field name="alpha" writable="1">
<type name="gfloat" c:type="float"/>
</field>
</record>
"""))
let record = try #require(repo.namespaces.first?.records.first)
#expect(record.fields.count == 2)
#expect(record.fields.allSatisfy { $0.type == .float })
#expect(record.isBoxed)
}
/// C vtable structs hold function-pointer members, spelled as a `<callback>`
/// child rather than a `<type>`. Such a field is an opaque function pointer
/// at the ABI level, and must not be left as `.void`.
@Test("Function-pointer fields are typed as pointers")
func callbackFieldsAreTypedAsPointers() throws {
let repo = try GIRParser().parse(
xmlString: girDocument(
"""
<record name="IOFuncs" c:type="GIOFuncs">
<field name="io_read">
<callback name="io_read">
<return-value transfer-ownership="none"><type name="gint"/></return-value>
<parameters>
<parameter name="channel"><type name="gpointer"/></parameter>
</parameters>
</callback>
</field>
<field name="depth"><type name="gint" c:type="int"/></field>
</record>
"""))
let ns = try #require(repo.namespaces.first)
let record = try #require(ns.records.first)
#expect(record.fields.count == 2)
#expect(record.fields[0].type == .pointer)
#expect(record.fields[1].type == .int32)
// A callback nested in a field is not a namespace-level declaration.
#expect(ns.callbacks.isEmpty)
}
@Test("Parameter direction and caller-allocates are captured")
func parameterDirectionIsCaptured() throws {
let repo = try GIRParser().parse(
xmlString: girDocument(
"""
<class name="Widget" c:type="GtkWidget">
<method name="measure" c:identifier="gtk_widget_measure">
<return-value transfer-ownership="none"><type name="none"/></return-value>
<parameters>
<instance-parameter name="widget"><type name="Widget"/></instance-parameter>
<parameter name="minimum" direction="out" caller-allocates="0">
<type name="gint" c:type="int*"/>
</parameter>
<parameter name="orientation" direction="in">
<type name="gint" c:type="int"/>
</parameter>
</parameters>
</method>
</class>
"""))
let method = try #require(repo.namespaces.first?.classes.first?.methods.first)
#expect(method.parameters.count == 3)
#expect(method.parameters[0].isInstanceParameter)
#expect(method.parameters[1].direction == .out)
#expect(!method.parameters[1].callerAllocates)
#expect(method.parameters[2].direction == .in)
}
@Test("GError-throwing callables are flagged")
func throwingCallablesAreFlagged() throws {
let repo = try GIRParser().parse(
xmlString: girDocument(
"""
<class name="File" c:type="GFile">
<method name="load" c:identifier="g_file_load" throws="1">
<return-value transfer-ownership="none"><type name="gboolean"/></return-value>
</method>
<method name="peek" c:identifier="g_file_peek">
<return-value transfer-ownership="none"><type name="gboolean"/></return-value>
</method>
</class>
"""))
let methods = try #require(repo.namespaces.first?.classes.first?.methods)
#expect(methods[0].throwsGError)
#expect(!methods[1].throwsGError)
}
@Test("Return value ownership and nullability are captured")
func returnValueSemanticsAreCaptured() throws {
let repo = try GIRParser().parse(
xmlString: girDocument(
"""
<class name="Widget" c:type="GtkWidget">
<method name="get_name" c:identifier="gtk_widget_get_name">
<return-value transfer-ownership="full" nullable="1">
<type name="utf8" c:type="char*"/>
</return-value>
</method>
</class>
"""))
let method = try #require(repo.namespaces.first?.classes.first?.methods.first)
#expect(method.returnValue.type == .string)
#expect(method.returnValue.transferOwnership == .full)
#expect(method.returnValue.isNullable)
}
@Test("Return value c:type is captured (pointer-to-scalar detection)")
func returnValueCTypeIsCaptured() throws {
let repo = try GIRParser().parse(
xmlString: girDocument(
"""
<record name="Pixbuf" c:type="GdkPixbuf">
<method name="read_pixels" c:identifier="gdk_pixbuf_read_pixels">
<return-value transfer-ownership="none">
<type name="guint8" c:type="const guint8*"/>
</return-value>
</method>
</record>
"""))
let method = try #require(repo.namespaces.first?.records.first?.methods.first)
#expect(method.returnValue.type == .uint8)
#expect(method.returnValue.cType == "const guint8*")
}
@Test("Non-introspectable and shadowed symbols are marked unbindable")
func symbolInfoIsCaptured() throws {
let repo = try GIRParser().parse(
xmlString: girDocument(
"""
<class name="Object" c:type="GObject">
<method name="set_valist" c:identifier="g_object_set_valist" introspectable="0">
<return-value transfer-ownership="none"><type name="none"/></return-value>
</method>
<method name="set" c:identifier="g_object_set" shadowed-by="set_property">
<return-value transfer-ownership="none"><type name="none"/></return-value>
</method>
<method name="get_data" c:identifier="g_object_get_data" deprecated="1" deprecated-version="2.4">
<return-value transfer-ownership="none"><type name="gpointer"/></return-value>
</method>
</class>
"""))
let methods = try #require(repo.namespaces.first?.classes.first?.methods)
#expect(!methods[0].symbolInfo.isIntrospectable)
#expect(!methods[0].symbolInfo.isBindable)
#expect(methods[1].symbolInfo.shadowedBy == "set_property")
#expect(!methods[1].symbolInfo.isBindable)
// Deprecation is recorded but does not by itself prevent binding.
#expect(methods[2].symbolInfo.isDeprecated)
#expect(methods[2].symbolInfo.deprecatedVersion == "2.4")
#expect(methods[2].symbolInfo.isBindable)
}
@Test("Class GType registration, finality, and interfaces are captured")
func classMetadataIsCaptured() throws {
let repo = try GIRParser().parse(
xmlString: girDocument(
"""
<class name="Widget" c:type="GtkWidget" parent="GObject.InitiallyUnowned"
abstract="1" glib:type-name="GtkWidget" glib:get-type="gtk_widget_get_type">
<implements name="Accessible"/>
<implements name="Buildable"/>
</class>
<class name="Label" c:type="GtkLabel" parent="Widget" final="1"
glib:type-name="GtkLabel" glib:get-type="gtk_label_get_type"/>
"""))
let classes = try #require(repo.namespaces.first?.classes)
#expect(classes[0].isAbstract)
#expect(!classes[0].isFinal)
#expect(classes[0].parent == "GObject.InitiallyUnowned")
#expect(classes[0].getTypeFunction == "gtk_widget_get_type")
#expect(classes[0].typeName == "GtkWidget")
#expect(classes[0].implements == ["Accessible", "Buildable"])
#expect(classes[1].isFinal)
}
@Test("GObject type-struct records are identified")
func gtypeStructRecordsAreIdentified() throws {
let repo = try GIRParser().parse(
xmlString: girDocument(
"""
<record name="WidgetClass" c:type="GtkWidgetClass" glib:is-gtype-struct-for="Widget"/>
<record name="RGBA" c:type="GdkRGBA" glib:get-type="gdk_rgba_get_type"/>
"""))
let records = try #require(repo.namespaces.first?.records)
#expect(records[0].isGTypeStructFor == "Widget")
#expect(!records[0].isBoxed)
#expect(records[1].isGTypeStructFor == nil)
#expect(records[1].isBoxed)
}
@Test("Array length metadata is captured")
func arrayLengthMetadataIsCaptured() throws {
let repo = try GIRParser().parse(
xmlString: girDocument(
"""
<class name="Widget" c:type="GtkWidget">
<method name="set_names" c:identifier="gtk_widget_set_names">
<return-value transfer-ownership="none"><type name="none"/></return-value>
<parameters>
<parameter name="names">
<array length="1" zero-terminated="0" c:type="char**">
<type name="utf8" c:type="char*"/>
</array>
</parameter>
<parameter name="n_names"><type name="gint" c:type="int"/></parameter>
<parameter name="tags">
<array zero-terminated="1" c:type="char**">
<type name="utf8" c:type="char*"/>
</array>
</parameter>
<parameter name="quad">
<array fixed-size="4" c:type="int*">
<type name="gint" c:type="int"/>
</array>
</parameter>
<parameter name="argv">
<array c:type="char**">
<type name="utf8" c:type="char*"/>
</array>
</parameter>
<parameter name="data">
<array length="1" c:type="const guchar*">
<type name="guint8"/>
</array>
</parameter>
</parameters>
</method>
</class>
"""))
let params = try #require(repo.namespaces.first?.classes.first?.methods.first?.parameters)
guard case .cArray(let element, let info) = params[0].type else {
Issue.record("expected a C array for 'names', got \(params[0].type)")
return
}
#expect(element == .string)
#expect(info.lengthParameterIndex == 1)
#expect(!info.isZeroTerminated)
#expect(info.hasKnownLength)
guard case .cArray(_, let zeroInfo) = params[2].type else {
Issue.record("expected a C array for 'tags'")
return
}
#expect(zeroInfo.isZeroTerminated)
#expect(zeroInfo.hasKnownLength)
guard case .cArray(_, let fixedInfo) = params[3].type else {
Issue.record("expected a C array for 'quad'")
return
}
#expect(fixedInfo.fixedSize == 4)
// No zero-terminated/length/fixed-size attribute at all: GIR omits
// zero-terminated for a plain NULL-terminated array (e.g. GStrv), so
// absence must default to true — not false.
guard case .cArray(_, let absentInfo) = params[4].type else {
Issue.record("expected a C array for 'argv'")
return
}
#expect(absentInfo.isZeroTerminated)
#expect(absentInfo.hasKnownLength)
// The element's own c:type is what distinguishes a pointer vector
// (`char**` of `char*`) from a contiguous value buffer (`const guchar*`
// of `guint8`); the parser must carry it through, and leave it empty
// when the GIR omits it.
#expect(info.elementCType == "char*")
guard case .cArray(_, let bufferInfo) = params[5].type else {
Issue.record("expected a C array for 'data'")
return
}
#expect(bufferInfo.elementCType == "")
#expect(bufferInfo.cType == "const guchar*")
}
@Test("Container element types are captured")
func containerElementTypesAreCaptured() throws {
let repo = try GIRParser().parse(
xmlString: girDocument(
"""
<class name="Container" c:type="GtkContainer">
<method name="get_children" c:identifier="gtk_container_get_children">
<return-value transfer-ownership="container">
<type name="GLib.List" c:type="GList*">
<type name="Widget"/>
</type>
</return-value>
</method>
</class>
"""))
let method = try #require(repo.namespaces.first?.classes.first?.methods.first)
guard case .container(let kind, let elements) = method.returnValue.type else {
Issue.record("expected a container type, got \(method.returnValue.type)")
return
}
#expect(kind == .list)
#expect(elements == [.typeRef("Widget")])
#expect(method.returnValue.transferOwnership == .container)
}
@Test("Callback scope, closure, and destroy indices are captured")
func callbackScopeIsCaptured() throws {
let repo = try GIRParser().parse(
xmlString: girDocument(
"""
<class name="Widget" c:type="GtkWidget">
<method name="add_tick" c:identifier="gtk_widget_add_tick_callback">
<return-value transfer-ownership="none"><type name="guint"/></return-value>
<parameters>
<parameter name="callback" scope="notified" closure="1" destroy="2">
<type name="TickCallback" c:type="GtkTickCallback"/>
</parameter>
<parameter name="user_data"><type name="gpointer"/></parameter>
<parameter name="notify"><type name="GLib.DestroyNotify"/></parameter>
</parameters>
</method>
</class>
"""))
let param = try #require(repo.namespaces.first?.classes.first?.methods.first?.parameters.first)
#expect(param.scope == .notified)
#expect(param.closureIndex == 1)
#expect(param.destroyIndex == 2)
}
@Test("Property getter, setter, and transfer are captured")
func propertyAccessorsAreCaptured() throws {
let repo = try GIRParser().parse(
xmlString: girDocument(
"""
<class name="Label" c:type="GtkLabel">
<property name="label" writable="1" transfer-ownership="none"
getter="get_label" setter="set_label">
<type name="utf8" c:type="gchar*"/>
</property>
</class>
"""))
let prop = try #require(repo.namespaces.first?.classes.first?.properties.first)
#expect(prop.type == .string)
#expect(prop.getter == "get_label")
#expect(prop.setter == "set_label")
#expect(prop.isWritable)
}
@Test("Interface prerequisites parse from child elements")
func interfacePrerequisitesParse() throws {
let repo = try GIRParser().parse(
xmlString: girDocument(
"""
<interface name="Buildable" c:type="GtkBuildable" glib:get-type="gtk_buildable_get_type">
<prerequisite name="GObject.Object"/>
</interface>
"""))
let iface = try #require(repo.namespaces.first?.interfaces.first)
#expect(iface.prereqs == ["GObject.Object"])
#expect(iface.getTypeFunction == "gtk_buildable_get_type")
}
@Test("Virtual method bodies do not leak into class members")
func virtualMethodsDoNotLeak() throws {
let repo = try GIRParser().parse(
xmlString: girDocument(
"""
<class name="Widget" c:type="GtkWidget">
<virtual-method name="snapshot">
<return-value transfer-ownership="none"><type name="none"/></return-value>
<parameters>
<instance-parameter name="widget"><type name="Widget"/></instance-parameter>
<parameter name="snapshot"><type name="Snapshot"/></parameter>
</parameters>
</virtual-method>
<method name="show" c:identifier="gtk_widget_show">
<return-value transfer-ownership="none"><type name="none"/></return-value>
</method>
</class>
"""))
let cls = try #require(repo.namespaces.first?.classes.first)
// The virtual method contributes no method, and — critically — its
// parameters do not attach to the sibling <method>.
#expect(cls.methods.count == 1)
#expect(cls.methods[0].name == "show")
#expect(cls.methods[0].parameters.isEmpty)
}
@Test("Enum GType, error domain, and aliased values are captured")
func enumMetadataIsCaptured() throws {
let repo = try GIRParser().parse(
xmlString: girDocument(
"""
<enumeration name="Align" c:type="GtkAlign" glib:type-name="GtkAlign"
glib:get-type="gtk_align_get_type">
<member name="fill" value="0" c:identifier="GTK_ALIGN_FILL"/>
<member name="baseline_fill" value="4" c:identifier="GTK_ALIGN_BASELINE_FILL"/>
<member name="baseline" value="4" c:identifier="GTK_ALIGN_BASELINE"/>
</enumeration>
<enumeration name="FileError" c:type="GFileError" glib:error-domain="g-file-error-quark">
<member name="exist" value="0" c:identifier="G_FILE_ERROR_EXIST"/>
</enumeration>
"""))
let enums = try #require(repo.namespaces.first?.enumerations)
#expect(enums[0].getTypeFunction == "gtk_align_get_type")
// Aliased raw values survive parsing intact; de-duplicating them into
// Swift `static var` aliases is the renderer's job.
#expect(enums[0].members.count == 3)
#expect(enums[0].members[1].value == "4")
#expect(enums[0].members[2].value == "4")
#expect(enums[1].errorDomain == "g-file-error-quark")
}
@Test("Full primitive type table maps without fabricating type refs")
func primitiveTypeTableIsComplete() {
let expected: [String: GIRType] = [
"none": .void, "gboolean": .boolean,
"gint8": .int8, "gint16": .int16, "gint": .int32, "gint32": .int32, "gint64": .int64,
"guint8": .uint8, "guint16": .uint16, "guint": .uint32, "guint32": .uint32, "guint64": .uint64,
"glong": .long, "gulong": .ulong, "gsize": .size, "gssize": .ssize,
"gchar": .char, "guchar": .uchar, "gunichar": .unichar, "GType": .gtype,
"gfloat": .float, "gdouble": .double,
"utf8": .string, "filename": .filename,
"gpointer": .pointer, "gconstpointer": .pointer, "va_list": .vaList,
]
for (name, type) in expected {
#expect(GIRXMLDelegate.girType(forName: name) == type, "\(name) should map to \(type)")
}
// Namespace-qualified references keep their namespace for the registry.
#expect(GIRXMLDelegate.girType(forName: "Gtk.Widget") == .typeRef("Widget", namespace: "Gtk"))
#expect(GIRXMLDelegate.girType(forName: "Widget") == .typeRef("Widget", namespace: nil))
}
}