1
0
Fork 0
gobject-generator/Tests/IntegrationTests/RealGIRParsingTests.swift

263 lines
12 KiB
Swift

// RealGIRParsingTests.swift
// Cross-checks the parser against the system's real .gir files. Counts are
// verified against independently-derived ground truth (grep over the same XML)
// so that a parser regression that silently drops constructs is caught here
// rather than surfacing as mysterious missing bindings downstream.
import Foundation
import Testing
@testable import GObjectGeneratorCore
@Suite("Real GIR parsing")
struct RealGIRParsingTests {
/// Directory holding the system's GObject Introspection files.
nonisolated static let girDirectory = "/usr/share/gir-1.0"
/// Whether a given GIR file is present on this system.
///
/// Callable from `@Test` availability traits, which run outside the
/// package's default MainActor isolation.
///
/// - Parameter name: The GIR file name, e.g. `"Gtk-4.0.gir"`.
/// - Returns: `true` when the file exists and tests depending on it can run.
nonisolated static func hasGIR(_ name: String) -> Bool {
FileManager.default.fileExists(atPath: "\(girDirectory)/\(name)")
}
/// Parses a system GIR file.
///
/// - Parameter name: The GIR file name, e.g. `"Gtk-4.0.gir"`.
/// - Returns: The parsed repository.
/// - Throws: A `GIRParserError` if parsing fails.
static func parse(_ name: String) throws -> Repository {
try GIRParser().parse(fileURL: URL(fileURLWithPath: "\(girDirectory)/\(name)"))
}
/// Counts occurrences of a literal substring in a GIR file, as independent
/// ground truth for what the parser should have found.
///
/// Mirrors `grep -c` semantics for the single-occurrence-per-line markup
/// these attributes appear in.
///
/// - Parameters:
/// - needle: The literal substring to count, e.g. `"direction=\"out\""`.
/// - file: The GIR file name.
/// - Returns: The number of lines containing the substring.
static func groundTruthCount(of needle: String, in file: String) throws -> Int {
let text = try String(contentsOfFile: "\(girDirectory)/\(file)", encoding: .utf8)
return text.split(separator: "\n").count { $0.contains(needle) }
}
@Test(.enabled(if: hasGIR("GLib-2.0.gir")))
func parsesGLib() throws {
let repo = try Self.parse("GLib-2.0.gir")
let ns = try #require(repo.namespaces.first { $0.name == "GLib" })
#expect(ns.version == "2.0")
#expect(ns.records.count > 50)
#expect(ns.functions.count > 100)
#expect(ns.enumerations.count > 10)
// Every record field must have a real type. `.void` fields were the
// signature of the old parser's dropped <type> routing.
let voidFields = ns.records.flatMap { record in
record.fields.filter { $0.type == .void }.map { "\(record.name).\($0.name)" }
}
#expect(voidFields.isEmpty, "record fields with no type: \(voidFields.prefix(5))")
}
@Test(.enabled(if: hasGIR("Gtk-4.0.gir")))
func parsesGtkMatchingGroundTruth() throws {
let repo = try Self.parse("Gtk-4.0.gir")
let ns = try #require(repo.namespaces.first { $0.name == "Gtk" })
#expect(ns.classes.count == (try Self.groundTruthCount(of: "<class ", in: "Gtk-4.0.gir")))
#expect(ns.records.count == (try Self.groundTruthCount(of: "<record ", in: "Gtk-4.0.gir")))
// GObject type structs (GtkWidgetClass and friends) dominate the record
// list and must be identified so the planner can skip them.
let typeStructs = ns.records.filter { $0.isGTypeStructFor != nil }
#expect(typeStructs.count == (try Self.groundTruthCount(of: "glib:is-gtype-struct-for", in: "Gtk-4.0.gir")))
#expect(typeStructs.count > ns.records.count / 2)
}
@Test(.enabled(if: hasGIR("Gtk-4.0.gir")))
func capturesOutParametersAndThrows() throws {
let repo = try Self.parse("Gtk-4.0.gir")
let ns = try #require(repo.namespaces.first { $0.name == "Gtk" })
/// Every callable reachable from the namespace, reduced to the two
/// facts under test: its parameters and whether it throws.
var parameterLists: [[Parameter]] = []
var throwsFlags: [Bool] = []
func record(_ parameters: [Parameter], _ throwsGError: Bool) {
parameterLists.append(parameters)
throwsFlags.append(throwsGError)
}
for cls in ns.classes {
for m in cls.methods { record(m.parameters, m.throwsGError) }
for c in cls.constructors { record(c.parameters, c.throwsGError) }
for f in cls.functions { record(f.parameters, f.throwsGError) }
}
for iface in ns.interfaces {
for m in iface.methods { record(m.parameters, m.throwsGError) }
}
for rec in ns.records {
for m in rec.methods { record(m.parameters, m.throwsGError) }
}
for f in ns.functions { record(f.parameters, f.throwsGError) }
let outParams = parameterLists.flatMap { $0 }.count { $0.direction == .out }
#expect(outParams > 300, "expected GTK's out-parameters to be parsed, found \(outParams)")
let throwing = throwsFlags.count { $0 }
#expect(throwing > 30, "expected GTK's throwing callables to be parsed, found \(throwing)")
// gtk_widget_measure is the canonical multi-out-parameter method.
let widget = try #require(ns.classes.first { $0.name == "Widget" })
let measure = try #require(widget.methods.first { $0.name == "measure" })
#expect(measure.parameters.count { $0.direction == .out } == 4)
}
@Test(.enabled(if: hasGIR("Gtk-4.0.gir")))
func capturesClassHierarchyAndGTypes() throws {
let repo = try Self.parse("Gtk-4.0.gir")
let ns = try #require(repo.namespaces.first { $0.name == "Gtk" })
let widget = try #require(ns.classes.first { $0.name == "Widget" })
#expect(widget.parent == "GObject.InitiallyUnowned")
#expect(widget.isAbstract)
#expect(widget.getTypeFunction == "gtk_widget_get_type")
#expect(widget.implements.contains("Accessible"))
// Every class must carry its GType registration: the registry needs it
// to classify types and drive GValue access.
let missingGType = ns.classes.filter { $0.getTypeFunction == nil }.map(\.name)
#expect(missingGType.isEmpty, "classes missing glib:get-type: \(missingGType.prefix(5))")
}
/// The registry must link the real GTK stack across module boundaries.
/// Under the old generator this chain was severed at every namespace
/// boundary, so `Gtk.Widget` did not inherit from `GObject.Object` at all.
@Test(
.enabled(
if: hasGIR("Gtk-4.0.gir") && hasGIR("Gio-2.0.gir")
&& hasGIR("GObject-2.0.gir") && hasGIR("GLib-2.0.gir")))
func registryLinksRealGtkStackAcrossModules() throws {
// Mirrors the tier configs: Gtk's stack is only coherent with Gio
// present, since Gtk.Application inherits Gio.Application.
let registry = TypeRegistry(repositories: [
"GLib": try Self.parse("GLib-2.0.gir"),
"GObject": try Self.parse("GObject-2.0.gir"),
"Gio": try Self.parse("Gio-2.0.gir"),
"Gtk": try Self.parse("Gtk-4.0.gir"),
])
let ancestry = registry.ancestry(of: "Gtk.Button").map(\.girName)
#expect(ancestry.contains("Gtk.Widget"))
#expect(ancestry.contains("GObject.InitiallyUnowned"))
#expect(ancestry.last == "GObject.Object")
// Floating-reference rule: widgets sink, plain GObjects do not.
let buttonFloats = registry.descendsFromInitiallyUnowned("Gtk.Button")
let appFloats = registry.descendsFromInitiallyUnowned("Gtk.Application")
#expect(buttonFloats)
#expect(!appFloats)
#expect(registry.isGObject("Gtk.Application"))
#expect(registry.ancestry(of: "Gtk.Application").map(\.girName).contains("Gio.Application"))
// Cross-module `open` requirement: GObject.Object is subclassed only
// from other modules, and must still be open.
let subclassed = registry.subclassedTypes()
#expect(subclassed.contains("GObject.Object"))
#expect(subclassed.contains("GObject.InitiallyUnowned"))
// Gtk includes Gdk and Gsk; with neither loaded, both must resolve as
// foreign so callables touching them skip cleanly rather than emitting
// fabricated type references.
let foreign = registry.foreign
#expect(foreign.contains("Gdk"))
#expect(foreign.contains("Gsk"))
let surface = registry.resolve(.typeRef("Surface", namespace: "Gdk"), from: "Gtk")
#expect(surface?.category == .foreign(namespace: "Gdk"))
}
/// cairo ships no GIR on this system, but Gdk references it. Loading Gdk
/// must therefore surface cairo as foreign rather than as an unknown type.
@Test(.enabled(if: hasGIR("Gdk-4.0.gir") && hasGIR("GObject-2.0.gir") && hasGIR("GLib-2.0.gir")))
func cairoResolvesAsForeignWhenGdkIsLoaded() throws {
let registry = TypeRegistry(repositories: [
"GLib": try Self.parse("GLib-2.0.gir"),
"GObject": try Self.parse("GObject-2.0.gir"),
"Gdk": try Self.parse("Gdk-4.0.gir"),
])
let foreign = registry.foreign
#expect(foreign.contains("cairo"))
let context = registry.resolve(.typeRef("Context", namespace: "cairo"), from: "Gdk")
#expect(context?.category == .foreign(namespace: "cairo"))
}
/// Every class parent named by the real GIRs must resolve. An unresolvable
/// parent means a fabricated superclass reference in generated code.
@Test(
.enabled(
if: hasGIR("Gtk-4.0.gir") && hasGIR("Gio-2.0.gir")
&& hasGIR("GObject-2.0.gir") && hasGIR("GLib-2.0.gir")))
func everyRealGtkClassParentResolves() throws {
let repos: [String: Repository] = [
"GLib": try Self.parse("GLib-2.0.gir"),
"GObject": try Self.parse("GObject-2.0.gir"),
"Gio": try Self.parse("Gio-2.0.gir"),
"Gtk": try Self.parse("Gtk-4.0.gir"),
]
let registry = TypeRegistry(repositories: repos)
let ns = try #require(repos["Gtk"]?.namespaces.first { $0.name == "Gtk" })
let unresolved = ns.classes.compactMap { cls -> String? in
guard let parent = cls.parent else { return nil }
let qualified = parent.contains(".") ? parent : "Gtk.\(parent)"
return registry.resolve(girName: qualified) == nil ? "\(cls.name) -> \(parent)" : nil
}
#expect(unresolved.isEmpty, "unresolvable class parents: \(unresolved.prefix(5))")
}
@Test(.enabled(if: hasGIR("Gtk-4.0.gir")))
func capturesNonIntrospectableSymbols() throws {
let repo = try Self.parse("Gtk-4.0.gir")
let ns = try #require(repo.namespaces.first { $0.name == "Gtk" })
let nonIntrospectable = ns.classes.flatMap { cls in
cls.methods.filter { !$0.symbolInfo.isIntrospectable }
}
#expect(!nonIntrospectable.isEmpty, "GTK has non-introspectable methods that must be recognized")
}
/// Every filename rendered from the real tier-1 GIRs must follow the
/// PascalCase convention the end-to-end lock that keeps future codegen
/// from reintroducing symbol-named files (`boxedFree.swift`,
/// `PARAM_MASK.swift`, `cclosureMarshalBOOLEANFLAGS.swift`).
@Test(.enabled(if: hasGIR("GLib-2.0.gir") && hasGIR("GObject-2.0.gir")))
func generatedFilenamesArePascalCase() throws {
let repositories = [
"GLib": try Self.parse("GLib-2.0.gir"),
"GObject": try Self.parse("GObject-2.0.gir"),
]
let analysis = MultiPackageAnalysis(
repositories: repositories,
directDependencies: [:], transitiveDependencies: [:],
implicitImports: [:], packageConfigs: [:]
)
let registry = TypeRegistry(repositories: repositories)
let plans = planModules(analysis: analysis, registry: registry)
#expect(plans.count == 2)
for (module, plan) in plans {
let files = renderModule(plan)
let violations = files.keys.filter { !isValidGeneratedFileName($0) }
#expect(violations.isEmpty,
"\(module) emitted non-PascalCase filenames: \(violations.sorted())")
// The merge targets must actually exist an empty module would
// vacuously pass the check above.
#expect(files["Functions.swift"] != nil)
#expect(files["Constants.swift"] != nil)
}
}
}