460 lines
25 KiB
Swift
460 lines
25 KiB
Swift
// PropertyGenerationTests.swift
|
|
// Covers Phase C6: GObject property planning and rendering. A property becomes
|
|
// a Swift computed `var` backed by the GValue machinery
|
|
// (`g_value_init` / `g_object_get_property` / `g_object_set_property`), rendered
|
|
// as a `public var` — for interfaces, as a protocol-extension default. Properties
|
|
// whose type has no `GValueOps` are recorded as skips.
|
|
//
|
|
// These tests also lock in the numeric bridging of enum/flags getters: a
|
|
// generated Swift enum uses an `Int` raw value while `g_value_get_enum` returns
|
|
// a `gint` (Int32), so the getter must bridge through `numericCast`. Emitting a
|
|
// bare `EnumType(rawValue: g_value_get_enum(...))` fails to compile — the
|
|
// regression that `enumPropertyGetterBridgesRawValueWithNumericCast` guards.
|
|
|
|
import Testing
|
|
|
|
@testable import GObjectGeneratorCore
|
|
|
|
@Suite("Property generation")
|
|
struct PropertyGenerationTests {
|
|
/// A GObject-local context: `currentModule == "GObject"` so mapped type
|
|
/// names are bare (e.g. `Object`, `Orientation`). Registers the named types
|
|
/// the property tests reference — a root object class, an enum, a bitfield,
|
|
/// and an interface (used to exercise the unsupported-type skip path).
|
|
func makeContext() -> MapContext {
|
|
let gobject = Repository(namespaces: [
|
|
Namespace(
|
|
name: "GObject", version: "2.0",
|
|
classes: [
|
|
Class(name: "Object", cType: "GObject", parent: nil,
|
|
getTypeFunction: "g_object_get_type"),
|
|
],
|
|
interfaces: [
|
|
Interface(name: "TypePlugin", cType: "GTypePlugin",
|
|
getTypeFunction: "g_type_plugin_get_type"),
|
|
],
|
|
records: [
|
|
Record(name: "Value", cType: "GValue",
|
|
getTypeFunction: "g_value_get_type",
|
|
copyFunction: "g_value_copy",
|
|
freeFunction: "g_value_free"),
|
|
],
|
|
enumerations: [
|
|
Enumeration(name: "Orientation", cType: "GtkOrientation",
|
|
members: [
|
|
EnumMember(name: "horizontal", value: "0",
|
|
cIdentifier: "GTK_ORIENTATION_HORIZONTAL"),
|
|
EnumMember(name: "vertical", value: "1",
|
|
cIdentifier: "GTK_ORIENTATION_VERTICAL"),
|
|
],
|
|
getTypeFunction: "gtk_orientation_get_type"),
|
|
],
|
|
bitfields: [
|
|
Bitfield(name: "StateFlags", cType: "GtkStateFlags",
|
|
members: [
|
|
EnumMember(name: "active", value: "1",
|
|
cIdentifier: "GTK_STATE_FLAG_ACTIVE"),
|
|
],
|
|
getTypeFunction: "gtk_state_flags_get_type"),
|
|
]
|
|
)
|
|
])
|
|
let registry = TypeRegistry(repositories: ["GObject": gobject])
|
|
return MapContext(registry: registry, currentModule: "GObject", currentNamespace: "GObject")
|
|
}
|
|
|
|
/// Plans a one-off class carrying `properties` and renders it to Swift
|
|
/// source, returning the class file body.
|
|
func renderClass(named name: String, properties: [Property],
|
|
methods: [Method] = []) -> (source: String, plan: ClassPlan, skips: [SkipEntry]) {
|
|
let klass = Class(name: name, cType: "G\(name)", parent: nil,
|
|
getTypeFunction: "g_\(name.lowercased())_get_type",
|
|
methods: methods, properties: properties)
|
|
let (plan, skips) = planClass(klass, context: makeContext())
|
|
let module = ModulePlan(module: "GObject", types: [.class(plan)], skips: skips,
|
|
coverage: CoverageStats())
|
|
return (renderModule(module)["\(name).swift"] ?? "", plan, skips)
|
|
}
|
|
|
|
/// A `self` instance parameter for building test methods.
|
|
var selfParam: Parameter {
|
|
Parameter(name: "self", type: .pointer, cType: "GThing*", isInstanceParameter: true)
|
|
}
|
|
|
|
/// Builds an instance getter method `get_<x>` returning `type`.
|
|
func getterMethod(_ girName: String, returns type: GIRType, nullable: Bool = false) -> Method {
|
|
Method(name: girName, cIdentifier: "g_thing_\(girName)",
|
|
parameters: [selfParam],
|
|
returnValue: ReturnValue(type: type, isNullable: nullable))
|
|
}
|
|
|
|
/// Builds an instance setter method `set_<x>` taking one argument of `type`.
|
|
/// `cType` must be a real C spelling (e.g. `"const char*"`) for string
|
|
/// arguments, which the parameter planner requires.
|
|
func setterMethod(_ girName: String, arg argName: String, of type: GIRType,
|
|
cType: String = "const char*", nullable: Bool = false) -> Method {
|
|
Method(name: girName, cIdentifier: "g_thing_\(girName)",
|
|
parameters: [selfParam, Parameter(name: argName, type: type, cType: cType, isNullable: nullable)],
|
|
returnValue: ReturnValue(type: .void))
|
|
}
|
|
|
|
// ── Primitive properties (the three the plan called out explicitly) ──
|
|
|
|
@Test("Readable Int32 property generates a GValue-backed getter")
|
|
func readableInt32Property() throws {
|
|
let prop = Property(name: "length", type: .int32, isReadable: true, isWritable: false)
|
|
let (source, plan, skips) = renderClass(named: "Buffer", properties: [prop])
|
|
#expect(skips.isEmpty)
|
|
#expect(plan.properties.count == 1)
|
|
#expect(source.contains("public var length: Int32"))
|
|
#expect(source.contains("g_value_init(&gvalue, gTypeInt)"))
|
|
#expect(source.contains("g_value_get_int(&gvalue)"))
|
|
#expect(source.contains("g_object_get_property(_instancePointer(pointer), \"length\", &gvalue)"))
|
|
// Read-only: no setter.
|
|
#expect(!source.contains("set {"))
|
|
#expect(!source.contains("g_object_set_property"))
|
|
}
|
|
|
|
// MARK: - Phase E4: no-unsafe-pointer public API policy
|
|
|
|
@Test("A root class's pointer storage and both instance inits are @_spi(SGTKInternal), the class itself stays public")
|
|
func classPointerSurfaceIsSPIGated() throws {
|
|
let prop = Property(name: "length", type: .int32, isReadable: true, isWritable: false)
|
|
let (source, _, _) = renderClass(named: "Buffer", properties: [prop])
|
|
#expect(source.contains("public class Buffer {"))
|
|
#expect(source.contains("@_spi(SGTKInternal) public let pointer: UnsafeMutableRawPointer"))
|
|
#expect(source.contains("@_spi(SGTKInternal) public required init(takingOwnership pointer: UnsafeMutableRawPointer)"))
|
|
#expect(source.contains("@_spi(SGTKInternal) public init(retaining pointer: UnsafeMutableRawPointer)"))
|
|
// The plain-public getter is unaffected — Int32 carries no raw pointer.
|
|
#expect(source.contains(" public var length: Int32"))
|
|
}
|
|
|
|
@Test("A root class's isolated deinit unrefs its pointer via g_object_unref")
|
|
func rootClassEmitsUnrefDeinit() throws {
|
|
let prop = Property(name: "length", type: .int32, isReadable: true, isWritable: false)
|
|
let (source, plan, _) = renderClass(named: "Buffer", properties: [prop])
|
|
#expect(plan.unrefFunc == "g_object_unref")
|
|
#expect(source.contains("isolated deinit {"))
|
|
#expect(source.contains("g_object_unref(pointer)"))
|
|
}
|
|
|
|
@Test("A class rooting a non-GObject fundamental unrefs through _instancePointer")
|
|
func fundamentalRootClassUnrefsTypedPointer() throws {
|
|
let plan = ClassPlan(
|
|
name: "ParamSpec", girName: "GObject.ParamSpec", cType: "GParamSpec",
|
|
getTypeFunction: "g_param_spec_get_type",
|
|
refFunc: "g_param_spec_ref", unrefFunc: "g_param_spec_unref"
|
|
)
|
|
let module = ModulePlan(module: "GObject", types: [.class(plan)], skips: [],
|
|
coverage: CoverageStats())
|
|
let source = renderModule(module)["ParamSpec.swift"] ?? ""
|
|
#expect(source.contains("isolated deinit {"))
|
|
#expect(source.contains("g_param_spec_unref(_instancePointer(pointer))"))
|
|
}
|
|
|
|
@Test("Writable string property generates both a getter and a setter")
|
|
func writableStringProperty() throws {
|
|
let prop = Property(name: "label", type: .string, isReadable: true, isWritable: true)
|
|
let (source, _, _) = renderClass(named: "Button", properties: [prop])
|
|
#expect(source.contains("public var label: String"))
|
|
#expect(source.contains("get {"))
|
|
#expect(source.contains("set {"))
|
|
#expect(source.contains("g_value_get_string(&gvalue)"))
|
|
// The string setter routes newValue through withCString.
|
|
#expect(source.contains("newValue.withCString"))
|
|
#expect(source.contains("g_value_set_string(&gvalue, cstr)"))
|
|
#expect(source.contains("g_object_set_property(_instancePointer(pointer), \"label\", &gvalue)"))
|
|
}
|
|
|
|
@Test("Read-only boolean property generates a getter only")
|
|
func readOnlyBooleanProperty() throws {
|
|
let prop = Property(name: "visible", type: .boolean, isReadable: true, isWritable: false)
|
|
let (source, _, _) = renderClass(named: "Widget", properties: [prop])
|
|
#expect(source.contains("public var visible: Bool"))
|
|
#expect(source.contains("g_value_get_boolean(&gvalue) != 0"))
|
|
#expect(!source.contains("set {"))
|
|
}
|
|
|
|
// ── Enum / flags numeric bridging (regression guard for the getter bug) ──
|
|
|
|
@Test("Enum property getter bridges the raw value through numericCast")
|
|
func enumPropertyGetterBridgesRawValueWithNumericCast() throws {
|
|
let prop = Property(name: "orientation",
|
|
type: .typeRef("Orientation", namespace: "GObject"),
|
|
isReadable: true, isWritable: false)
|
|
let (source, plan, skips) = renderClass(named: "Orientable", properties: [prop])
|
|
#expect(skips.isEmpty)
|
|
#expect(plan.properties.count == 1)
|
|
#expect(source.contains("public var orientation: Orientation"))
|
|
#expect(source.contains("g_value_init(&gvalue, gTypeEnum)"))
|
|
// `g_value_get_enum` returns Int32; the enum's raw value is Int, so the
|
|
// result MUST be numericCast — a bare cast would not compile.
|
|
#expect(source.contains("Orientation(rawValue: numericCast(g_value_get_enum(&gvalue)))!"))
|
|
#expect(!source.contains("rawValue: g_value_get_enum(&gvalue))!"))
|
|
}
|
|
|
|
@Test("Flags property getter bridges the raw value through numericCast")
|
|
func flagsPropertyGetterBridgesRawValueWithNumericCast() throws {
|
|
let prop = Property(name: "state-flags",
|
|
type: .typeRef("StateFlags", namespace: "GObject"),
|
|
isReadable: true, isWritable: false)
|
|
let (source, _, skips) = renderClass(named: "Widget", properties: [prop])
|
|
#expect(skips.isEmpty)
|
|
#expect(source.contains("public var stateFlags: StateFlags"))
|
|
#expect(source.contains("g_value_init(&gvalue, gTypeFlags)"))
|
|
#expect(source.contains("StateFlags(rawValue: numericCast(g_value_get_flags(&gvalue)))"))
|
|
}
|
|
|
|
// ── Ownership, construct-only, naming, and object properties ──
|
|
|
|
@Test("Object property getter takes a retained reference (transfer none)")
|
|
func objectPropertyGetterUsesRetaining() throws {
|
|
let prop = Property(name: "child",
|
|
type: .typeRef("Object", namespace: "GObject"),
|
|
isReadable: true, isWritable: false)
|
|
let (source, _, skips) = renderClass(named: "Bin", properties: [prop])
|
|
#expect(skips.isEmpty)
|
|
#expect(source.contains("public var child: Object"))
|
|
#expect(source.contains("g_value_init(&gvalue, gTypeObject)"))
|
|
#expect(source.contains("Object(retaining: g_value_get_object(&gvalue))"))
|
|
}
|
|
|
|
@Test("Construct-only properties are treated as read-only")
|
|
func constructOnlyPropertyIsReadOnly() throws {
|
|
// Writable in GIR, but construct-only: a runtime setter would silently
|
|
// no-op, so it must render as read-only.
|
|
let prop = Property(name: "source", type: .string,
|
|
isReadable: true, isWritable: true, isConstructOnly: true)
|
|
let (source, plan, _) = renderClass(named: "Binding", properties: [prop])
|
|
#expect(plan.properties.count == 1)
|
|
#expect(plan.properties[0].setter == nil)
|
|
#expect(source.contains("public var source: String"))
|
|
#expect(!source.contains("set {"))
|
|
#expect(!source.contains("g_object_set_property"))
|
|
}
|
|
|
|
@Test("A kebab-case GIR name yields a camelCase Swift name and a preserved GObject name")
|
|
func girNamePreservedWhileSwiftNameCamelCased() throws {
|
|
let prop = Property(name: "source-property", type: .string,
|
|
isReadable: true, isWritable: false)
|
|
let (source, plan, _) = renderClass(named: "Binding", properties: [prop])
|
|
#expect(plan.properties[0].swiftName == "sourceProperty")
|
|
#expect(plan.properties[0].girName == "source-property")
|
|
#expect(source.contains("public var sourceProperty: String"))
|
|
// The original kebab-case name is what GObject knows the property by.
|
|
#expect(source.contains("g_object_get_property(_instancePointer(pointer), \"source-property\", &gvalue)"))
|
|
}
|
|
|
|
// ── Skips and filtering ──
|
|
|
|
@Test("A property whose type has no GValue support is skipped, not rendered")
|
|
func unsupportedPropertyTypeIsSkipped() throws {
|
|
// Interface-typed properties have no GValueOps mapping.
|
|
let prop = Property(name: "plugin",
|
|
type: .typeRef("TypePlugin", namespace: "GObject"),
|
|
isReadable: true, isWritable: false)
|
|
let (source, plan, skips) = renderClass(named: "Loader", properties: [prop])
|
|
#expect(plan.properties.isEmpty)
|
|
#expect(skips.count == 1)
|
|
#expect(skips[0].reason == .unsupportedGValueCategory)
|
|
#expect(!source.contains("var plugin"))
|
|
}
|
|
|
|
@Test("Non-bindable properties are excluded entirely")
|
|
func nonBindablePropertyIsExcluded() throws {
|
|
let prop = Property(name: "internalThing", type: .int32,
|
|
isReadable: true, isWritable: false,
|
|
symbolInfo: SymbolInfo(isIntrospectable: false))
|
|
let (source, plan, skips) = renderClass(named: "Widget", properties: [prop])
|
|
// Not bindable => never even considered: no plan entry, no skip.
|
|
#expect(plan.properties.isEmpty)
|
|
#expect(skips.isEmpty)
|
|
#expect(!source.contains("internalThing"))
|
|
}
|
|
|
|
// ── Interface property extension defaults ──
|
|
|
|
@Test("A readable-only interface property becomes a GValue-backed extension default getter")
|
|
func interfaceReadOnlyPropertyIsExtensionDefault() throws {
|
|
let iface = Interface(
|
|
name: "Orientable", cType: "GtkOrientable",
|
|
properties: [
|
|
Property(name: "orientation",
|
|
type: .typeRef("Orientation", namespace: "GObject"),
|
|
isReadable: true, isWritable: false),
|
|
]
|
|
)
|
|
let (plan, skips) = planInterface(iface, context: makeContext())
|
|
#expect(skips.isEmpty)
|
|
#expect(plan.properties.count == 1)
|
|
let module = ModulePlan(module: "GObject", types: [.interface(plan)], skips: [],
|
|
coverage: CoverageStats())
|
|
let source = renderModule(module)["Orientable.swift"] ?? ""
|
|
// Protocol body is bare — only the `pointer` requirement.
|
|
#expect(!source.contains("var orientation: Orientation { get }"))
|
|
// A default implementation, in a protocol extension, with a real body.
|
|
#expect(source.contains("extension Orientable {"))
|
|
#expect(source.contains("public var orientation: Orientation"))
|
|
#expect(source.contains("g_object_get_property"))
|
|
}
|
|
|
|
@Test("A writable interface property becomes a GValue-backed extension default getter+setter")
|
|
func interfaceWritablePropertyIsExtensionDefault() throws {
|
|
let iface = Interface(
|
|
name: "Editable", cType: "GtkEditable",
|
|
properties: [
|
|
Property(name: "text", type: .string, isReadable: true, isWritable: true),
|
|
]
|
|
)
|
|
let (plan, _) = planInterface(iface, context: makeContext())
|
|
let module = ModulePlan(module: "GObject", types: [.interface(plan)], skips: [],
|
|
coverage: CoverageStats())
|
|
let source = renderModule(module)["Editable.swift"] ?? ""
|
|
#expect(!source.contains("var text: String { get set }"))
|
|
#expect(source.contains("extension Editable {"))
|
|
#expect(source.contains("public var text: String"))
|
|
#expect(source.contains("g_object_set_property"))
|
|
}
|
|
|
|
// ── getter=/setter= method delegation ──
|
|
|
|
@Test("A read+write property with getter=/setter= delegates to those methods")
|
|
func readWriteDelegatesToAccessorMethods() throws {
|
|
let prop = Property(name: "label", type: .string, isReadable: true, isWritable: true,
|
|
getter: "get_label", setter: "set_label")
|
|
let (source, plan, _) = renderClass(
|
|
named: "Button", properties: [prop],
|
|
methods: [getterMethod("get_label", returns: .string),
|
|
setterMethod("set_label", arg: "label", of: .string)])
|
|
// The accessor forwards to the generated methods; no GValue machinery.
|
|
#expect(plan.properties[0].getter == .delegate(method: "getLabel", argumentLabel: nil))
|
|
#expect(plan.properties[0].setter == .delegate(method: "setLabel", argumentLabel: "label"))
|
|
#expect(source.contains("public var label: String {"))
|
|
#expect(source.contains("getLabel()"))
|
|
#expect(source.contains("setLabel(label: newValue)"))
|
|
#expect(!source.contains("g_object_get_property"))
|
|
#expect(!source.contains("g_object_set_property"))
|
|
}
|
|
|
|
@Test("A read-only property with getter= delegates to the getter method")
|
|
func readOnlyDelegatesToGetterMethod() throws {
|
|
let prop = Property(name: "count", type: .int32, isReadable: true, isWritable: false,
|
|
getter: "get_count")
|
|
let (source, plan, _) = renderClass(
|
|
named: "Model", properties: [prop],
|
|
methods: [getterMethod("get_count", returns: .int32)])
|
|
#expect(plan.properties[0].getter == .delegate(method: "getCount", argumentLabel: nil))
|
|
#expect(plan.properties[0].setter == nil)
|
|
#expect(source.contains("public var count: Int32"))
|
|
#expect(source.contains("getCount()"))
|
|
#expect(!source.contains("g_object_get_property"))
|
|
}
|
|
|
|
@Test("A delegated getter inherits the method's nullability")
|
|
func delegatedGetterInheritsNullability() throws {
|
|
// The getter method returns Object? (nullable) — the property type must
|
|
// follow it, which the uniform GValue path (non-optional Object) would
|
|
// get wrong and could trap.
|
|
let prop = Property(name: "source",
|
|
type: .typeRef("Object", namespace: "GObject"),
|
|
isReadable: true, isWritable: false, getter: "get_source")
|
|
let (source, plan, _) = renderClass(
|
|
named: "Binding", properties: [prop],
|
|
methods: [getterMethod("get_source",
|
|
returns: .typeRef("Object", namespace: "GObject"),
|
|
nullable: true)])
|
|
#expect(plan.properties[0].swiftType == "Object?")
|
|
#expect(source.contains("public var source: Object?"))
|
|
#expect(source.contains("getSource()"))
|
|
}
|
|
|
|
@Test("Falls back to GValue when the named getter= method is unavailable")
|
|
func fallsBackToGValueWhenMethodMissing() throws {
|
|
// getter= names a method that was never planned (not in the class), so
|
|
// the accessor must fall back to the GValue machinery.
|
|
let prop = Property(name: "orientation",
|
|
type: .typeRef("Orientation", namespace: "GObject"),
|
|
isReadable: true, isWritable: false, getter: "get_orientation")
|
|
let (source, plan, _) = renderClass(named: "Orientable", properties: [prop], methods: [])
|
|
if case .gvalue = plan.properties[0].getter {} else {
|
|
Issue.record("expected GValue fallback, got \(plan.properties[0].getter)")
|
|
}
|
|
#expect(source.contains("g_value_init(&gvalue, gTypeEnum)"))
|
|
#expect(source.contains("numericCast(g_value_get_enum(&gvalue))"))
|
|
}
|
|
|
|
@Test("Falls back to GValue when delegated getter and setter types disagree")
|
|
func fallsBackToGValueOnTypeMismatch() throws {
|
|
// Nullable getter (String?) but non-null setter (String): delegating
|
|
// both would give the computed property two different types, so the
|
|
// planner uses GValue for both instead.
|
|
let prop = Property(name: "text", type: .string, isReadable: true, isWritable: true,
|
|
getter: "get_text", setter: "set_text")
|
|
let (source, plan, _) = renderClass(
|
|
named: "Entry", properties: [prop],
|
|
methods: [getterMethod("get_text", returns: .string, nullable: true),
|
|
setterMethod("set_text", arg: "text", of: .string, nullable: false)])
|
|
if case .gvalue = plan.properties[0].getter {} else {
|
|
Issue.record("expected GValue fallback on type mismatch")
|
|
}
|
|
#expect(source.contains("g_object_get_property"))
|
|
#expect(source.contains("g_object_set_property"))
|
|
}
|
|
|
|
// ── C6 regression: g_value_unset in setter + boxed pointer + nullable string ──
|
|
|
|
@Test("Getter always emits g_value_unset")
|
|
func getterEmitsGValueUnset() throws {
|
|
let prop = Property(name: "length", type: .int32, isReadable: true, isWritable: false)
|
|
let (source, _, _) = renderClass(named: "Buffer", properties: [prop])
|
|
// The getter body ends with g_value_unset before return.
|
|
#expect(source.contains("g_value_unset"))
|
|
}
|
|
|
|
@Test("Writable string property setter emits g_value_unset after g_object_set_property")
|
|
func setterEmitsGValueUnset() throws {
|
|
let prop = Property(name: "label", type: .string, isReadable: true, isWritable: true)
|
|
let (source, _, _) = renderClass(named: "Button", properties: [prop])
|
|
// Each setter call should be followed by a g_value_unset.
|
|
#expect(source.contains("g_value_unset"))
|
|
// (This test is specifically for the setter path — the getter already had it.)
|
|
}
|
|
|
|
@Test("Boxed GValue setter passes .pointer, not the wrapper")
|
|
func boxedSetterUsesPointer() throws {
|
|
// Use a Value (boxed) type for a writable property.
|
|
let prop = Property(name: "data",
|
|
type: .typeRef("Value", namespace: "GObject"),
|
|
isReadable: true, isWritable: true)
|
|
let (source, _, _) = renderClass(named: "Holder", properties: [prop])
|
|
#expect(source.contains("g_value_set_boxed(&gvalue, newValue.pointer)"))
|
|
#expect(!source.contains("g_value_set_boxed(&gvalue, newValue)"))
|
|
}
|
|
|
|
@Test("Nullable string getter uses .map to handle NULL")
|
|
func nullableStringGetterUsesMap() throws {
|
|
let prop = Property(name: "title", type: .string, isReadable: true, isWritable: false,
|
|
isNullable: true)
|
|
let (source, plan, _) = renderClass(named: "Window", properties: [prop])
|
|
#expect(plan.properties[0].swiftType == "String?")
|
|
#expect(source.contains("g_value_get_string(&gvalue).map { String(cString: $0) }"))
|
|
}
|
|
|
|
// MARK: - E2: property/method name collision (Pango.FontFamily.isVariable)
|
|
|
|
@Test("A property whose Swift name collides with a planned method is dropped, keeping the method")
|
|
func propertyCollidingWithMethodIsSkipped() throws {
|
|
// GIR `is-variable` property and `is_variable` method both yield the
|
|
// Swift name `isVariable` — `var isVariable: Bool` and
|
|
// `func isVariable() -> Bool` are an invalid redeclaration.
|
|
let prop = Property(name: "is-variable", type: .boolean, isReadable: true, isWritable: false)
|
|
let method = getterMethod("is_variable", returns: .boolean)
|
|
let (source, plan, skips) = renderClass(named: "FontFamily", properties: [prop], methods: [method])
|
|
|
|
#expect(plan.properties.isEmpty)
|
|
#expect(plan.methods.contains { $0.name == "isVariable" })
|
|
#expect(skips.contains { $0.reason == .nameCollision })
|
|
#expect(!source.contains("public var isVariable"))
|
|
#expect(source.contains("func isVariable("))
|
|
}
|
|
}
|