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

598 lines
30 KiB
Swift

// RendererCallableTests.swift
// Renderer-level tests for callable features that the compiler gate alone
// cannot catch: throws signatures, out-param tuple ordering, constructor
// error handling, and the memory-safety caller-allocates skip.
import Testing
@testable import GObjectGeneratorCore
@Suite("Renderer callables")
struct RendererCallableTests {
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"),
Class(name: "InitiallyUnowned", cType: "GInitiallyUnowned",
parent: "Object",
getTypeFunction: "g_initially_unowned_get_type"),
]
)
])
let registry = TypeRegistry(repositories: ["GObject": gobject])
return MapContext(registry: registry, currentModule: "GObject", currentNamespace: "GObject")
}
/// Builds a context with an ordinary GObject and a non-GObject
/// fundamental class that declares its own ref function.
func makeTransferContext() -> MapContext {
let gobject = Repository(namespaces: [
Namespace(
name: "GObject", version: "2.0",
classes: [
Class(name: "Object", cType: "GObject", parent: nil,
getTypeFunction: "g_object_get_type"),
Class(name: "Expr", cType: "GExpr", parent: nil,
getTypeFunction: "g_expr_get_type",
refFunc: "g_expr_ref", unrefFunc: "g_expr_unref"),
]
)
])
let registry = TypeRegistry(repositories: ["GObject": gobject])
return MapContext(registry: registry, currentModule: "GObject", currentNamespace: "GObject")
}
func renderCallable(_ plan: CallablePlan) -> String {
let module = ModulePlan(
module: "GObject",
types: [.callable(plan)],
skips: [],
coverage: CoverageStats()
)
return renderModule(module)["Functions.swift"] ?? ""
}
// MARK: - C2: Throws + result! unwrap
@Test("Throwing function returning object emits throws, &error, and result!")
func throwingFunctionEmitsResultUnwrap() throws {
// A function that returns Object and throws.
let fn = GlobalFunction(
name: "load_object", cIdentifier: "g_load_object",
parameters: [Parameter(name: "path", type: .string, cType: "const char*")],
returnValue: ReturnValue(type: .typeRef("Object", namespace: "GObject")),
throwsGError: true
)
guard case .success(let plan) = planFunction(fn, context: makeContext()) else {
Issue.record("expected load_object to plan successfully"); return
}
let body = renderCallable(plan)
#expect(body.contains("throws"))
#expect(body.contains("&error"))
// Pointer-returning throwing function: result! unwrap before adoption
#expect(body.contains("result!") || body.contains("marshalReturn"))
}
// MARK: - Phase E4: no-unsafe-pointer public API policy
@Test("A function with a gpointer parameter renders @_spi(SGTKInternal) public func, hiding the pointer")
func gpointerParameterHidesFunctionBehindSPI() throws {
let fn = GlobalFunction(
name: "set_user_data", cIdentifier: "g_set_user_data",
parameters: [Parameter(name: "data", type: .pointer, cType: "gpointer")],
returnValue: ReturnValue(type: .void)
)
guard case .success(let plan) = planFunction(fn, context: makeContext()) else {
Issue.record("expected set_user_data to plan successfully"); return
}
let body = renderCallable(plan)
#expect(body.contains("@_spi(SGTKInternal) public func setUserData"))
}
@Test("A function returning a plain wrapper type (no raw pointer in its signature) stays plain public")
func wrapperReturnStaysPlainPublic() throws {
let fn = GlobalFunction(
name: "get_default_object", cIdentifier: "g_get_default_object",
parameters: [],
returnValue: ReturnValue(type: .typeRef("Object", namespace: "GObject"))
)
guard case .success(let plan) = planFunction(fn, context: makeContext()) else {
Issue.record("expected get_default_object to plan successfully"); return
}
let body = renderCallable(plan)
#expect(body.contains("public func getDefaultObject"))
#expect(!body.contains("@_spi(SGTKInternal) public func getDefaultObject"))
}
// MARK: - transfer-ownership="full" object in-parameters
@Test("transfer-full object in-param refs before the call; transfer-none does not")
func transferFullObjectParamAddsRef() throws {
let ctx = makeTransferContext()
let borrowed = GlobalFunction(
name: "set_thing", cIdentifier: "g_set_thing",
parameters: [Parameter(name: "thing", type: .typeRef("Object", namespace: "GObject"),
cType: "GObject*", transferOwnership: .none)],
returnValue: ReturnValue(type: .void))
let consumed = GlobalFunction(
name: "take_thing", cIdentifier: "g_take_thing",
parameters: [Parameter(name: "thing", type: .typeRef("Object", namespace: "GObject"),
cType: "GObject*", transferOwnership: .full)],
returnValue: ReturnValue(type: .void))
guard case .success(let borrowedPlan) = planFunction(borrowed, context: ctx),
case .success(let consumedPlan) = planFunction(consumed, context: ctx) else {
Issue.record("expected both functions to plan successfully")
return
}
#expect(renderCallable(borrowedPlan).contains(
"g_set_thing(_instancePointer(thing.pointer))"))
#expect(renderCallable(consumedPlan).contains(
"g_take_thing(_instancePointer(_rawPointer(g_object_ref(_instancePointer(thing.pointer)))))"))
}
@Test("transfer-full nullable object in-param refs inside the map closure")
func transferFullNullableObjectParamRefsInsideMap() throws {
let fn = GlobalFunction(
name: "take_maybe", cIdentifier: "g_take_maybe",
parameters: [Parameter(name: "thing", type: .typeRef("Object", namespace: "GObject"),
cType: "GObject*", isNullable: true, transferOwnership: .full)],
returnValue: ReturnValue(type: .void))
guard case .success(let plan) = planFunction(fn, context: makeTransferContext()) else {
Issue.record("expected g_take_maybe to plan successfully")
return
}
#expect(renderCallable(plan).contains(
"thing.map { _instancePointer(_rawPointer(g_object_ref(_instancePointer($0.pointer)))) }"))
}
@Test("transfer-full param of a non-GObject fundamental uses its own ref function")
func transferFullFundamentalUsesOwnRefFunction() throws {
let fn = GlobalFunction(
name: "take_expr", cIdentifier: "g_take_expr",
parameters: [Parameter(name: "expr", type: .typeRef("Expr", namespace: "GObject"),
cType: "GExpr*", transferOwnership: .full)],
returnValue: ReturnValue(type: .void))
guard case .success(let plan) = planFunction(fn, context: makeTransferContext()) else {
Issue.record("expected g_take_expr to plan successfully")
return
}
let body = renderCallable(plan)
#expect(body.contains(
"g_take_expr(_instancePointer(_rawPointer(g_expr_ref(_instancePointer(expr.pointer)))))"))
#expect(!body.contains("g_object_ref"))
}
// MARK: - C3: Out-param tuples
@Test("Single out-param with no Swift return becomes the out-param's type")
func singleOutParamNoReturn() throws {
// A function that returns void but has one out-param.
let fn = GlobalFunction(
name: "get_data", cIdentifier: "g_get_data",
parameters: [
Parameter(name: "key", type: .string, cType: "const char*"),
Parameter(name: "result", type: .string, cType: "char**", direction: .out),
],
returnValue: ReturnValue(type: .void)
)
guard case .success(let plan) = planFunction(fn, context: makeContext()) else {
Issue.record("expected get_data to plan successfully"); return
}
let body = renderCallable(plan)
// Single out-param return: the type directly (no tuple label)
#expect(body.contains("func getData(key: String) -> String"))
}
@Test("Multiple out-params with no Swift return produce a tuple")
func multiOutParamNoReturnTuple() throws {
let fn = GlobalFunction(
name: "get_coords", cIdentifier: "g_get_coords",
parameters: [
Parameter(name: "id", type: .int32, cType: "gint"),
Parameter(name: "x", type: .int32, cType: "gint", direction: .out),
Parameter(name: "y", type: .int32, cType: "gint", direction: .out),
],
returnValue: ReturnValue(type: .void)
)
guard case .success(let plan) = planFunction(fn, context: makeContext()) else {
Issue.record("expected get_coords to plan successfully"); return
}
let body = renderCallable(plan)
#expect(body.contains("func getCoords(id: Int32) -> (x: Int32, y: Int32)"))
}
@Test("Out-param local type follows the c:type: mutable char** vs const char**")
func outParamLocalTypeFollowsCType() throws {
let mutableFn = GlobalFunction(
name: "next_token", cIdentifier: "g_next_token",
parameters: [
Parameter(name: "input", type: .string, cType: "const char*"),
Parameter(name: "endptr", type: .string, cType: "char**", direction: .out),
],
returnValue: ReturnValue(type: .void)
)
guard case .success(let mutablePlan) = planFunction(mutableFn, context: makeContext()) else {
Issue.record("expected next_token to plan successfully"); return
}
#expect(renderCallable(mutablePlan).contains("UnsafeMutablePointer<CChar>?"))
let constFn = GlobalFunction(
name: "peek_token", cIdentifier: "g_peek_token",
parameters: [
Parameter(name: "input", type: .string, cType: "const char*"),
Parameter(name: "endptr", type: .string, cType: "const char**", direction: .out),
],
returnValue: ReturnValue(type: .void)
)
guard case .success(let constPlan) = planFunction(constFn, context: makeContext()) else {
Issue.record("expected peek_token to plan successfully"); return
}
#expect(renderCallable(constPlan).contains("UnsafePointer<CChar>?"))
#expect(!renderCallable(constPlan).contains("UnsafeMutablePointer<CChar>?"))
}
@Test("Swift return with out-params produces a labeled tuple")
func returnWithOutParamsLabeledTuple() throws {
let fn = GlobalFunction(
name: "find_item", cIdentifier: "g_find_item",
parameters: [
Parameter(name: "id", type: .int32, cType: "gint"),
Parameter(name: "index", type: .int32, cType: "gint", direction: .out),
],
returnValue: ReturnValue(type: .boolean)
)
guard case .success(let plan) = planFunction(fn, context: makeContext()) else {
Issue.record("expected find_item to plan successfully"); return
}
let body = renderCallable(plan)
#expect(body.contains("-> (return: Bool, index: Int32)"))
}
// MARK: - Memory-safety caller-allocates skip
@Test("Caller-allocates out-param is skipped with the callerAllocates reason")
func callerAllocatesSkip() throws {
let fn = GlobalFunction(
name: "unichar_to_utf8", cIdentifier: "g_unichar_to_utf8",
parameters: [
Parameter(name: "c", type: .unichar, cType: "gunichar"),
Parameter(name: "outbuf", type: .string, cType: "char*", direction: .out,
callerAllocates: true),
]
)
guard case .skip(let entry) = planFunction(fn, context: makeContext()) else {
Issue.record("expected caller-allocates function to be skipped"); return
}
#expect(entry.reason == .outParameter)
#expect(entry.detail.contains("caller-allocates"))
}
// MARK: - Throwing constructor
@Test("Throwing constructor emits throws, &error, GLibError, and self.init(takingOwnership:)")
func throwingConstructorEmitsErrorHandling() throws {
// Construct a class with a throwing constructor.
let klass = Class(
name: "File", cType: "GFile", parent: "Object",
getTypeFunction: "g_file_get_type",
constructors: [
Constructor(name: "new", cIdentifier: "g_file_new",
parameters: [
Parameter(name: "path", type: .string, cType: "const char*"),
],
returnValue: ReturnValue(
transferOwnership: .full),
throwsGError: true),
]
)
let (plan, _) = planClass(klass, context: makeContext())
let module = ModulePlan(module: "GObject", types: [.class(plan)], skips: [],
coverage: CoverageStats())
let files = renderModule(module)
let src = files["File.swift"] ?? ""
#expect(src.contains("convenience init(path: String) throws"))
#expect(src.contains("&error"))
#expect(src.contains("throw GLibError(consuming: e)"))
#expect(src.contains("self.init(takingOwnership:"))
}
// MARK: - Out-param constructor skip
@Test("Constructor with out-params is skipped with constructorOutParams")
func outParamConstructorSkipped() throws {
let klass = Class(
name: "Stream", cType: "GStream", parent: "Object",
getTypeFunction: "g_stream_get_type",
constructors: [
Constructor(name: "new", cIdentifier: "g_stream_new",
parameters: [
Parameter(name: "name", type: .string, cType: "const char*"),
Parameter(name: "fd", type: .int32, cType: "gint",
direction: .out),
],
returnValue: ReturnValue(
transferOwnership: .full)),
]
)
let (plan, skips) = planClass(klass, context: makeContext())
// Constructor should be skipped no constructor plan, skip entry recorded.
#expect(plan.constructors.isEmpty)
let outSkip = skips.first { $0.reason == .constructorOutParams }
#expect(outSkip != nil)
}
// MARK: - E1: throwing constructor with string params doesn't nest self.init
@Test("Throwing constructor with a String param threads the result out of withCString instead of nesting self.init")
func throwingConstructorWithStringParamAvoidsNestedSelfInit() throws {
// `self.init` (a delegating initializer call) is illegal inside any
// closure, including a non-escaping `withCString` trailing closure.
// Regression guard for the tier-2 DBusConnection/DBusProxy bug where
// string-parameterised throwing constructors called self.init from
// inside the withCString closure.
let klass = Class(
name: "Proxy", cType: "GProxy", parent: "Object",
getTypeFunction: "g_proxy_get_type",
constructors: [
Constructor(name: "new_for_bus_sync", cIdentifier: "g_proxy_new_for_bus_sync",
parameters: [
Parameter(name: "name", type: .string, cType: "const char*"),
],
returnValue: ReturnValue(transferOwnership: .full),
throwsGError: true),
]
)
let (plan, _) = planClass(klass, context: makeContext())
let module = ModulePlan(module: "GObject", types: [.class(plan)], skips: [],
coverage: CoverageStats())
let src = renderModule(module)["Proxy.swift"] ?? ""
// The C call (returning the result) lives INSIDE the withCString
// closure as its trailing/implicit-return expression...
#expect(src.contains("g_proxy_new_for_bus_sync(cString0, &error)"))
// ...while `self.init` runs OUTSIDE every closure, never nested.
#expect(!src.contains("cString0, &error)\n self.init"))
let lines = src.components(separatedBy: "\n")
guard let selfInitLine = lines.first(where: { $0.contains("self.init(takingOwnership:") }) else {
Issue.record("expected a self.init(takingOwnership:) line"); return
}
// Indentation of 8 spaces == top-level init body scope, not nested
// one level inside the withCString closure (12 spaces).
#expect(selfInitLine.hasPrefix(" self.init"))
}
// MARK: - E1: constructor signature deduplication
@Test("Two constructors with the same rendered init signature but different Swift names dedup to one, keeping the first")
func constructorsWithIdenticalSignatureDedup() throws {
// `init(...)` never carries the constructor's Swift name (it always
// renders as bare `init`), so two GIR constructors mapping to
// DIFFERENT Swift names (`newFinish` vs `newForAddressFinish`) but
// the IDENTICAL parameter/throws/return shape still collide at
// `init(res:)`. Regression guard for the tier-2 DBusConnection bug.
let resParam = Parameter(name: "res", type: .typeRef("AsyncResult", namespace: "GObject"),
cType: "GAsyncResult*")
let klass = Class(
name: "Connection", cType: "GConnection", parent: "Object",
getTypeFunction: "g_connection_get_type",
constructors: [
Constructor(name: "new_finish", cIdentifier: "g_connection_new_finish",
parameters: [resParam],
returnValue: ReturnValue(transferOwnership: .full),
throwsGError: true),
Constructor(name: "new_for_address_finish", cIdentifier: "g_connection_new_for_address_finish",
parameters: [resParam],
returnValue: ReturnValue(transferOwnership: .full),
throwsGError: true),
]
)
let ctx = MapContext(
registry: TypeRegistry(repositories: ["GObject": Repository(namespaces: [
Namespace(name: "GObject", version: "2.0", classes: [
Class(name: "Object", cType: "GObject", parent: nil, getTypeFunction: "g_object_get_type"),
Class(name: "AsyncResult", cType: "GAsyncResult", parent: "Object",
getTypeFunction: "g_async_result_get_type"),
])
])]),
currentModule: "GObject", currentNamespace: "GObject"
)
let (plan, skips) = planClass(klass, context: ctx)
#expect(plan.constructors.count == 1)
#expect(plan.constructors[0].cIdentifier == "g_connection_new_finish")
let dedupSkip = skips.first { $0.reason == .nameCollision }
#expect(dedupSkip?.cIdentifier == "g_connection_new_for_address_finish")
let module = ModulePlan(module: "GObject", types: [.class(plan)], skips: [],
coverage: CoverageStats())
let src = renderModule(module)["Connection.swift"] ?? ""
let occurrences = src.components(separatedBy: "convenience init(res: AsyncResult) throws").count - 1
#expect(occurrences == 1)
}
// MARK: - E1: pointer out-param zero-initializes to nil, not 0
@Test("A gpointer out-param local variable initializes to nil, not the integer 0")
func pointerOutParamInitializesToNil() throws {
// `.direct` marshalIn covers both numeric out-params (`gint*` `0`)
// and raw-pointer out-params (`gpointer*` `UnsafeMutableRawPointer?`,
// which `= 0` cannot initialize). Regression guard for the tier-2
// FileInfo.getAttributeData bug.
let fn = GlobalFunction(
name: "get_attribute_data", cIdentifier: "g_file_info_get_attribute_data",
parameters: [
Parameter(name: "attribute", type: .string, cType: "const char*"),
Parameter(name: "value_pp", type: .pointer, cType: "gpointer*", direction: .out),
],
returnValue: ReturnValue(type: .void)
)
guard case .success(let plan) = planFunction(fn, context: makeContext()) else {
Issue.record("expected get_attribute_data to plan successfully"); return
}
let body = renderCallable(plan)
#expect(body.contains("var out0: UnsafeMutableRawPointer? = nil"))
#expect(!body.contains("var out0: UnsafeMutableRawPointer? = 0"))
}
// MARK: - E2: no unconditional Foundation import (collides with Gio.InputStream)
@Test("Generated module and support headers do not import Foundation")
func generatedHeadersOmitFoundationImport() throws {
let module = ModulePlan(module: "GLib", types: [], skips: [], coverage: CoverageStats())
let files = renderModule(module)
for (name, content) in files {
#expect(!content.contains("import Foundation"), "\(name) unexpectedly imports Foundation")
}
}
// MARK: - E3: nullable string constructor params
@Test("Constructor with nullable string parameter generates String? and _withOptionalCString")
func nullableStringConstructorParam() throws {
let klass = Class(
name: "Alert", cType: "GAlert", parent: "Object",
getTypeFunction: "g_alert_get_type",
constructors: [
Constructor(name: "new", cIdentifier: "g_alert_new",
parameters: [
Parameter(name: "message", type: .string,
cType: "const char*", isNullable: true),
],
returnValue: ReturnValue(transferOwnership: .full)),
]
)
let (plan, _) = planClass(klass, context: makeContext())
let module = ModulePlan(module: "GObject", types: [.class(plan)], skips: [],
coverage: CoverageStats())
let src = renderModule(module)["Alert.swift"] ?? ""
// The signature must show String? for the nullable string parameter.
#expect(src.contains("convenience init(message: String?)"))
// The body wraps the nullable param with _withOptionalCString.
#expect(src.contains("_withOptionalCString(message)"))
// The convenience init must NOT be SPI-gated (check the init line is not preceded by @_spi).
let initLines = src.components(separatedBy: "\n")
let convenienceLines = initLines.filter { $0.contains("convenience init") }
#expect(!convenienceLines.isEmpty, "expected a convenience init line")
for line in convenienceLines {
#expect(!line.contains("@_spi"), "convenience init should not be SPI-gated, got: \(line)")
}
// Uses takingOwnership for transfer-ownership="full".
if !src.contains("self.init(takingOwnership:") {
Issue.record("expected self.init(takingOwnership:) for transfer-ownership=full")
}
}
@Test("Constructor with mixed nullable and required string params generates correct bridging")
func constructorWithMixedNullableAndRequiredParams() throws {
let klass = Class(
name: "Dialog", cType: "GAlertDialog", parent: "Object",
getTypeFunction: "g_alert_dialog_get_type",
constructors: [
Constructor(name: "new", cIdentifier: "g_alert_dialog_new",
parameters: [
Parameter(name: "heading", type: .string,
cType: "const char*", isNullable: true),
Parameter(name: "body", type: .string,
cType: "const char*"),
],
returnValue: ReturnValue(transferOwnership: .full)),
]
)
let (plan, _) = planClass(klass, context: makeContext())
let module = ModulePlan(module: "GObject", types: [.class(plan)], skips: [],
coverage: CoverageStats())
let src = renderModule(module)["Dialog.swift"] ?? ""
// The signature must show String? for the nullable param and String for the required param.
#expect(src.contains("convenience init(heading: String?, body: String)"))
// The nullable param gets _withOptionalCString, the required param gets .withCString.
#expect(src.contains("_withOptionalCString(heading)"))
#expect(src.contains("body.withCString"))
// The convenience init line must NOT be SPI-gated (the class file has SPI-gated
// designated inits, but the convenience init itself is public).
let initLines2 = src.components(separatedBy: "\n")
let convenienceLines2 = initLines2.filter { $0.contains("convenience init") }
#expect(!convenienceLines2.isEmpty, "expected a convenience init line")
for line in convenienceLines2 {
#expect(!line.contains("@_spi"), "convenience init should not be SPI-gated, got: \(line)")
}
}
// MARK: - E3: constructor borrowing ownership
@Test("Constructor with ownership .none return generates convenience init calling self.init(retaining:)")
func constructorBorrowedReturnUsesRetaining() throws {
let klass = Class(
name: "Borrowed", cType: "GBorrowed", parent: "Object",
getTypeFunction: "g_borrowed_get_type",
constructors: [
Constructor(name: "new", cIdentifier: "g_borrowed_new",
parameters: [
Parameter(name: "name", type: .string,
cType: "const char*"),
],
returnValue: ReturnValue(transferOwnership: .none)),
]
)
let (plan, _) = planClass(klass, context: makeContext())
let module3 = ModulePlan(module: "GObject", types: [.class(plan)], skips: [],
coverage: CoverageStats())
let src = renderModule(module3)["Borrowed.swift"] ?? ""
#expect(src.contains("self.init(retaining:"), "expected self.init(retaining:) for transfer-ownership=none, got: \(src)")
#expect(!src.contains("self.init(takingOwnership:"), "should NOT use init(takingOwnership:) for transfer-ownership=none")
// The convenience init line must NOT be SPI-gated.
let initLines3 = src.components(separatedBy: "\n")
let convenienceLines3 = initLines3.filter { $0.contains("convenience init") }
for line in convenienceLines3 {
#expect(!line.contains("@_spi"), "convenience init should not be SPI-gated, got: \(line)")
}
}
// MARK: - C array in-parameter bridging
@Test("An object-pointer array renders _withPointerArray and elides the length parameter")
func objectArrayParameterRendersPointerArray() throws {
let fn = GlobalFunction(
name: "replace", cIdentifier: "g_replace",
parameters: [
Parameter(name: "pages",
type: .cArray(.typeRef("Object", namespace: "GObject"),
ArrayInfo(lengthParameterIndex: 1, cType: "GObject**",
elementCType: "GObject*")),
cType: "GObject**"),
Parameter(name: "n_pages", type: .int32, cType: "int"),
],
returnValue: ReturnValue(type: .void)
)
guard case .success(let plan) = planFunction(fn, context: makeContext()) else {
Issue.record("expected replace to plan successfully"); return
}
let body = renderCallable(plan)
#expect(body.contains("_withPointerArray(pages.map { $0.pointer })"))
#expect(body.contains("numericCast(pages.count)"))
#expect(body.contains("pages: [Object]"))
// The length arg is synthesized from `pages.count`, so it must not
// appear in the Swift signature.
#expect(!body.contains("nPages:"))
}
@Test("A const scalar buffer renders _withScalarArray and a typed Swift array")
func scalarArrayParameterRendersScalarArray() throws {
let fn = GlobalFunction(
name: "base64_encode", cIdentifier: "g_base64_encode",
parameters: [
Parameter(name: "data",
type: .cArray(.uint8, ArrayInfo(lengthParameterIndex: 1,
cType: "const guchar*")),
cType: "const guchar*"),
Parameter(name: "len", type: .size, cType: "gsize"),
],
returnValue: ReturnValue(type: .string)
)
guard case .success(let plan) = planFunction(fn, context: makeContext()) else {
Issue.record("expected base64_encode to plan successfully"); return
}
let body = renderCallable(plan)
#expect(body.contains("_withScalarArray("))
#expect(body.contains("data: [UInt8]"))
#expect(body.contains("numericCast(data.count)"))
}
}